mirror of https://github.com/artifacthub/hub.git
Store and serve packages logos (#93)
Closes #26 Signed-off-by: Sergio Castaño Arteaga <tegioz@icloud.com>
This commit is contained in:
parent
568b2acec8
commit
accd31b3ff
|
|
@ -17,3 +17,4 @@ stringData:
|
|||
tracker:
|
||||
numWorkers: {{ .Values.chartTracker.numWorkers }}
|
||||
repositoriesNames: {{ .Values.chartTracker.repositories }}
|
||||
imageStore: {{ .Values.chartTracker.imageStore }}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ chartTracker:
|
|||
repository: tegioz/chart-tracker
|
||||
numWorkers: 50
|
||||
repositories: []
|
||||
imageStore: pg
|
||||
|
||||
dbMigrator:
|
||||
image:
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
type job struct {
|
||||
repo *hub.ChartRepository
|
||||
chartVersion *repo.ChartVersion
|
||||
downloadLogo bool
|
||||
}
|
||||
|
||||
// dispatcher is in charge of generating jobs and dispatching them among the
|
||||
|
|
@ -112,12 +113,17 @@ func (d *dispatcher) trackRepositoryCharts(wg *sync.WaitGroup, r *hub.ChartRepos
|
|||
return
|
||||
}
|
||||
for _, chartVersions := range indexFile.Entries {
|
||||
for _, chartVersion := range chartVersions {
|
||||
for i, chartVersion := range chartVersions {
|
||||
var downloadLogo bool
|
||||
if i == 0 {
|
||||
downloadLogo = true
|
||||
}
|
||||
key := fmt.Sprintf("%s@%s", chartVersion.Metadata.Name, chartVersion.Metadata.Version)
|
||||
if chartVersion.Digest != packagesDigest[key] {
|
||||
d.Queue <- &job{
|
||||
repo: r,
|
||||
chartVersion: chartVersion,
|
||||
downloadLogo: downloadLogo,
|
||||
}
|
||||
}
|
||||
select {
|
||||
|
|
|
|||
|
|
@ -36,12 +36,16 @@ func main() {
|
|||
log.Info().Msg("Chart tracker shutting down..")
|
||||
}()
|
||||
|
||||
// Setup database and hub api
|
||||
// Setup database, hub api and image store
|
||||
db, err := util.SetupDB(cfg)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Database setup failed")
|
||||
}
|
||||
hubApi := hub.New(db)
|
||||
imageStore, err := util.SetupImageStore(cfg, db)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("ImageStore setup failed")
|
||||
}
|
||||
|
||||
// Launch dispatcher and workers and wait for them to finish
|
||||
var wg sync.WaitGroup
|
||||
|
|
@ -49,7 +53,7 @@ func main() {
|
|||
wg.Add(1)
|
||||
go dispatcher.run(&wg, cfg.GetStringSlice("tracker.repositoriesNames"))
|
||||
for i := 0; i < cfg.GetInt("tracker.numWorkers"); i++ {
|
||||
w := newWorker(ctx, i, hubApi)
|
||||
w := newWorker(ctx, i, hubApi, imageStore)
|
||||
wg.Add(1)
|
||||
go w.run(&wg, dispatcher.Queue)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
|
@ -12,6 +16,7 @@ import (
|
|||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/tegioz/hub/internal/hub"
|
||||
"github.com/tegioz/hub/internal/img"
|
||||
"helm.sh/helm/v3/pkg/chart"
|
||||
"helm.sh/helm/v3/pkg/chart/loader"
|
||||
)
|
||||
|
|
@ -21,17 +26,19 @@ type worker struct {
|
|||
ctx context.Context
|
||||
id int
|
||||
hubApi *hub.Hub
|
||||
imageStore img.Store
|
||||
logger zerolog.Logger
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// newWorker creates a new worker instance.
|
||||
func newWorker(ctx context.Context, id int, hubApi *hub.Hub) *worker {
|
||||
func newWorker(ctx context.Context, id int, hubApi *hub.Hub, imageStore img.Store) *worker {
|
||||
return &worker{
|
||||
ctx: ctx,
|
||||
id: id,
|
||||
hubApi: hubApi,
|
||||
logger: log.With().Int("worker", id).Logger(),
|
||||
ctx: ctx,
|
||||
id: id,
|
||||
hubApi: hubApi,
|
||||
imageStore: imageStore,
|
||||
logger: log.With().Int("worker", id).Logger(),
|
||||
httpClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
|
|
@ -96,34 +103,41 @@ func (w *worker) handleJob(j *job) error {
|
|||
}
|
||||
|
||||
// Load chart from remote archive in memory
|
||||
resp, err := w.httpClient.Get(u)
|
||||
chart, err := w.loadChart(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 400 {
|
||||
w.logger.Warn().
|
||||
Str("repo", j.repo.Name).
|
||||
Str("chart", j.chartVersion.Metadata.Name).
|
||||
Str("version", j.chartVersion.Metadata.Version).
|
||||
Str("url", u).
|
||||
Int("code", resp.StatusCode).
|
||||
Send()
|
||||
Msg("Chart load failed")
|
||||
return nil
|
||||
}
|
||||
chart, err := loader.LoadArchive(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
md := chart.Metadata
|
||||
|
||||
// Store chart logo when available if requested
|
||||
var imageID string
|
||||
if j.downloadLogo {
|
||||
if md.Icon != "" {
|
||||
data, err := w.downloadImage(md.Icon)
|
||||
if err != nil {
|
||||
w.logger.Debug().Err(err).Str("url", md.Icon).Msg("Image download failed")
|
||||
} else {
|
||||
imageID, err = w.imageStore.SaveImage(w.ctx, data)
|
||||
if err != nil && !errors.Is(err, image.ErrFormat) {
|
||||
w.logger.Warn().Err(err).Str("url", md.Icon).Msg("Save image failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare hub package to be registered
|
||||
md := chart.Metadata
|
||||
p := &hub.Package{
|
||||
Kind: hub.Chart,
|
||||
Name: md.Name,
|
||||
Description: md.Description,
|
||||
HomeURL: md.Home,
|
||||
LogoURL: md.Icon,
|
||||
ImageID: imageID,
|
||||
Keywords: md.Keywords,
|
||||
Version: md.Version,
|
||||
AppVersion: md.AppVersion,
|
||||
|
|
@ -151,6 +165,36 @@ func (w *worker) handleJob(j *job) error {
|
|||
return w.hubApi.RegisterPackage(w.ctx, p)
|
||||
}
|
||||
|
||||
// loadChart loads a chart from a remote archive located at the url provided.
|
||||
func (w *worker) loadChart(u string) (*chart.Chart, error) {
|
||||
resp, err := w.httpClient.Get(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
chart, err := loader.LoadArchive(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return chart, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected status code received: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// downloadImage downloads the image located at the url provided.
|
||||
func (w *worker) downloadImage(u string) ([]byte, error) {
|
||||
resp, err := w.httpClient.Get(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return ioutil.ReadAll(resp.Body)
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected status code received: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// getFile returns the file requested from the provided chart.
|
||||
func getFile(chart *chart.Chart, name string) *chart.File {
|
||||
for _, file := range chart.Files {
|
||||
|
|
|
|||
|
|
@ -7,29 +7,39 @@ import (
|
|||
"path"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
svg "github.com/h2non/go-is-svg"
|
||||
"github.com/jackc/pgx/v4"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/rs/zerolog/hlog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/tegioz/hub/internal/hub"
|
||||
"github.com/tegioz/hub/internal/img/pg"
|
||||
)
|
||||
|
||||
// handlers groups all the http handlers defined for the hub, including the
|
||||
// router in charge of sending requests to the right handler.
|
||||
type handlers struct {
|
||||
cfg *viper.Viper
|
||||
hubApi *hub.Hub
|
||||
router http.Handler
|
||||
cfg *viper.Viper
|
||||
hubApi *hub.Hub
|
||||
imageStore *pg.ImageStore
|
||||
router http.Handler
|
||||
|
||||
mu sync.RWMutex
|
||||
imagesCache map[string][]byte
|
||||
}
|
||||
|
||||
// setupHandlers creates a new handlers instance.
|
||||
func setupHandlers(cfg *viper.Viper, hubApi *hub.Hub) *handlers {
|
||||
func setupHandlers(cfg *viper.Viper, hubApi *hub.Hub, imageStore *pg.ImageStore) *handlers {
|
||||
h := &handlers{
|
||||
cfg: cfg,
|
||||
hubApi: hubApi,
|
||||
cfg: cfg,
|
||||
hubApi: hubApi,
|
||||
imageStore: imageStore,
|
||||
imagesCache: make(map[string][]byte),
|
||||
}
|
||||
h.setupRouter()
|
||||
return h
|
||||
|
|
@ -47,8 +57,11 @@ func (h *handlers) setupRouter() {
|
|||
r.GET("/api/v1/stats", h.getStats)
|
||||
r.GET("/api/v1/updates", h.getPackagesUpdates)
|
||||
r.GET("/api/v1/search", h.search)
|
||||
r.GET("/api/v1/package/:package_id", h.getPackage)
|
||||
r.GET("/api/v1/package/:package_id/:version", h.getPackageVersion)
|
||||
r.GET("/api/v1/package/:packageID", h.getPackage)
|
||||
r.GET("/api/v1/package/:packageID/:version", h.getPackageVersion)
|
||||
|
||||
// Images
|
||||
r.GET("/image/:image", h.images)
|
||||
|
||||
// Static files
|
||||
staticFilesPath := path.Join(h.cfg.GetString("server.webBuildPath"), "static")
|
||||
|
|
@ -69,6 +82,7 @@ func (h *handlers) getStats(w http.ResponseWriter, r *http.Request, _ httprouter
|
|||
if err != nil {
|
||||
log.Error().Err(err).Msg("getStats failed")
|
||||
http.Error(w, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
renderJSON(w, jsonData)
|
||||
}
|
||||
|
|
@ -80,6 +94,7 @@ func (h *handlers) getPackagesUpdates(w http.ResponseWriter, r *http.Request, _
|
|||
if err != nil {
|
||||
log.Error().Err(err).Msg("getPackagesUpdates failed")
|
||||
http.Error(w, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
renderJSON(w, jsonData)
|
||||
}
|
||||
|
|
@ -122,21 +137,23 @@ func (h *handlers) search(w http.ResponseWriter, r *http.Request, _ httprouter.P
|
|||
if err != nil {
|
||||
log.Error().Err(err).Str("query", r.URL.RawQuery).Msg("search failed")
|
||||
http.Error(w, "", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
renderJSON(w, jsonData)
|
||||
}
|
||||
|
||||
// getPackage is an http handler used to get a package details.
|
||||
func (h *handlers) getPackage(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
packageID := ps.ByName("package_id")
|
||||
packageID := ps.ByName("packageID")
|
||||
jsonData, err := h.hubApi.GetPackageJSON(r.Context(), packageID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
http.NotFound(w, r)
|
||||
} else {
|
||||
log.Error().Err(err).Str("packageID", packageID).Msg("getPackage failed")
|
||||
http.Error(w, "", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
renderJSON(w, jsonData)
|
||||
}
|
||||
|
|
@ -144,12 +161,12 @@ func (h *handlers) getPackage(w http.ResponseWriter, r *http.Request, ps httprou
|
|||
// getPackageVersion is an http handler used to get the details of a package
|
||||
// specific version.
|
||||
func (h *handlers) getPackageVersion(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
packageID := ps.ByName("package_id")
|
||||
packageID := ps.ByName("packageID")
|
||||
version := ps.ByName("version")
|
||||
jsonData, err := h.hubApi.GetPackageVersionJSON(r.Context(), packageID, version)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
http.NotFound(w, r)
|
||||
} else {
|
||||
log.Error().Err(err).
|
||||
Str("packageID", packageID).
|
||||
|
|
@ -157,10 +174,58 @@ func (h *handlers) getPackageVersion(w http.ResponseWriter, r *http.Request, ps
|
|||
Msg("getPackageVersion failed")
|
||||
http.Error(w, "", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
renderJSON(w, jsonData)
|
||||
}
|
||||
|
||||
// images in an http handler that serves images stored in the database.
|
||||
func (h *handlers) images(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
// Extract image id and version
|
||||
image := ps.ByName("image")
|
||||
parts := strings.Split(image, "@")
|
||||
var imageID, version string
|
||||
if len(parts) == 2 {
|
||||
imageID = parts[0]
|
||||
version = parts[1]
|
||||
} else {
|
||||
imageID = image
|
||||
}
|
||||
|
||||
// Check if image version data is cached
|
||||
h.mu.RLock()
|
||||
data, ok := h.imagesCache[image]
|
||||
h.mu.RUnlock()
|
||||
if !ok {
|
||||
// Get image data from database
|
||||
var err error
|
||||
data, err = h.imageStore.GetImage(r.Context(), imageID, version)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
http.NotFound(w, r)
|
||||
} else {
|
||||
log.Error().Err(err).Str("imageID", imageID).Send()
|
||||
http.Error(w, "", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Save image data in cache
|
||||
h.mu.Lock()
|
||||
h.imagesCache[image] = data
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
// Set headers and write image data to response writer
|
||||
w.Header().Set("Cache-Control", "max-age=31536000")
|
||||
if svg.Is(data) {
|
||||
w.Header().Set("Content-Type", "image/svg+xml")
|
||||
} else {
|
||||
w.Header().Set("Content-Type", http.DetectContentType(data))
|
||||
}
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
// serveIndex is an http handler that serves the index.html file.
|
||||
func (h *handlers) serveIndex(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate")
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/tegioz/hub/internal/hub"
|
||||
"github.com/tegioz/hub/internal/img/pg"
|
||||
"github.com/tegioz/hub/internal/util"
|
||||
)
|
||||
|
||||
|
|
@ -24,12 +25,13 @@ func main() {
|
|||
log.Fatal().Err(err).Msg("Logger setup failed")
|
||||
}
|
||||
|
||||
// Setup hub api instance
|
||||
// Setup hub api and image store instances
|
||||
db, err := util.SetupDB(cfg)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Database setup failed")
|
||||
}
|
||||
hubApi := hub.New(db)
|
||||
imageStore := pg.NewImageStore(db)
|
||||
|
||||
// Setup and launch server
|
||||
addr := cfg.GetString("server.addr")
|
||||
|
|
@ -38,7 +40,7 @@ func main() {
|
|||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 5 * time.Second,
|
||||
IdleTimeout: 1 * time.Minute,
|
||||
Handler: setupHandlers(cfg, hubApi).router,
|
||||
Handler: setupHandlers(cfg, hubApi, imageStore).router,
|
||||
}
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
|
|
|
|||
|
|
@ -10,3 +10,4 @@ db:
|
|||
tracker:
|
||||
numWorkers: 50
|
||||
repositoriesNames: []
|
||||
imageStore: pg
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ create table if not exists package (
|
|||
display_name text check (display_name <> ''),
|
||||
description text check (description <> ''),
|
||||
home_url text check (home_url <> ''),
|
||||
logo_url text check (logo_url <> ''),
|
||||
image_id uuid,
|
||||
keywords text[],
|
||||
latest_version text not null check (latest_version <> ''),
|
||||
created_at timestamptz default current_timestamp not null,
|
||||
|
|
@ -83,12 +83,26 @@ create table if not exists package__maintainer (
|
|||
primary key (package_id, maintainer_id)
|
||||
);
|
||||
|
||||
create table if not exists image (
|
||||
image_id uuid primary key default uuid_generate_v4(),
|
||||
original_hash bytea not null check (original_hash <> '') unique
|
||||
);
|
||||
|
||||
create table if not exists image_version (
|
||||
image_id uuid not null references image on delete cascade,
|
||||
version text not null check (version <> ''),
|
||||
data bytea not null,
|
||||
primary key (image_id, version)
|
||||
);
|
||||
|
||||
{{ template "functions/_create_all_functions.sql" }}
|
||||
|
||||
---- create above / drop below ----
|
||||
|
||||
{{ template "functions/_drop_all_functions.sql" }}
|
||||
|
||||
drop table if exists image_version;
|
||||
drop table if exists image;
|
||||
drop table if exists package__maintainer;
|
||||
drop table if exists maintainer;
|
||||
drop table if exists snapshot;
|
||||
|
|
|
|||
|
|
@ -8,3 +8,5 @@
|
|||
{{ template "functions/get_package_version.sql" }}
|
||||
{{ template "functions/get_package.sql" }}
|
||||
{{ template "functions/get_packages_updates.sql" }}
|
||||
{{ template "functions/register_image.sql" }}
|
||||
{{ template "functions/get_image.sql" }}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
drop function if exists get_image(p_image_id uuid, p_version text);
|
||||
drop function if exists register_image(p_original_hash bytea, p_version text, p_data bytea);
|
||||
drop function if exists get_packages_updates();
|
||||
drop function if exists get_package(p_package_id uuid);
|
||||
drop function if exists get_package_version(p_package_id uuid, version text);
|
||||
drop function if exists search_packages(query jsonb);
|
||||
drop function if exists get_package_version(p_package_id uuid, p_version text);
|
||||
drop function if exists search_packages(p_query jsonb);
|
||||
drop function if exists get_stats();
|
||||
drop function if exists register_package(p_pkg jsonb);
|
||||
drop function if exists get_chart_repository_packages_digest(p_chart_repository_id uuid);
|
||||
drop function if exists get_chart_repository_by_name(name text);
|
||||
drop function if exists get_chart_repository_by_name(p_name text);
|
||||
drop function if exists get_chart_repositories();
|
||||
drop function if exists semver_gte(v1 text, v2 text);
|
||||
drop function if exists semver_gte(p_v1 text, p_v2 text);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
-- get_image returns the image identified by the id and version provided.
|
||||
create or replace function get_image(p_image_id uuid, p_version text)
|
||||
returns setof bytea as $$
|
||||
select data
|
||||
from image_version
|
||||
where image_id = p_image_id
|
||||
and
|
||||
case when p_version <> '' and exists (
|
||||
select data from image_version
|
||||
where image_id = p_image_id
|
||||
and version = p_version
|
||||
) then
|
||||
version = p_version
|
||||
else
|
||||
version = (
|
||||
select version from image_version
|
||||
where image_id = p_image_id
|
||||
order by version asc limit 1
|
||||
)
|
||||
end;
|
||||
$$ language sql;
|
||||
|
|
@ -9,7 +9,7 @@ returns setof json as $$
|
|||
'display_name', p.display_name,
|
||||
'description', p.description,
|
||||
'home_url', p.home_url,
|
||||
'logo_url', p.logo_url,
|
||||
'image_id', p.image_id,
|
||||
'keywords', p.keywords,
|
||||
'readme', s.readme,
|
||||
'links', s.links,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ returns setof json as $$
|
|||
'kind', package_kind_id,
|
||||
'name', name,
|
||||
'display_name', display_name,
|
||||
'logo_url', logo_url,
|
||||
'image_id', image_id,
|
||||
'app_version', app_version,
|
||||
'chart_repository', (
|
||||
select json_build_object(
|
||||
|
|
@ -24,7 +24,7 @@ returns setof json as $$
|
|||
p.package_kind_id,
|
||||
p.name,
|
||||
p.display_name,
|
||||
p.logo_url,
|
||||
p.image_id,
|
||||
s.app_version,
|
||||
r.name as chart_repository_name,
|
||||
r.display_name as chart_repository_display_name
|
||||
|
|
@ -41,7 +41,7 @@ returns setof json as $$
|
|||
'kind', package_kind_id,
|
||||
'name', name,
|
||||
'display_name', display_name,
|
||||
'logo_url', logo_url,
|
||||
'image_id', image_id,
|
||||
'app_version', app_version,
|
||||
'chart_repository', (
|
||||
select json_build_object(
|
||||
|
|
@ -56,7 +56,7 @@ returns setof json as $$
|
|||
p.package_kind_id,
|
||||
p.name,
|
||||
p.display_name,
|
||||
p.logo_url,
|
||||
p.image_id,
|
||||
s.app_version,
|
||||
r.name as chart_repository_name,
|
||||
r.display_name as chart_repository_display_name
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
-- register_image registers the provided image in the database.
|
||||
create or replace function register_image(p_original_hash bytea, p_version text, p_data bytea)
|
||||
returns setof uuid as $$
|
||||
declare
|
||||
v_image_id uuid;
|
||||
begin
|
||||
-- Get parent image id or register it if needed
|
||||
select image_id into v_image_id
|
||||
from image
|
||||
where original_hash = p_original_hash;
|
||||
if not found then
|
||||
insert into image (original_hash) values (p_original_hash)
|
||||
on conflict do nothing
|
||||
returning image_id into v_image_id;
|
||||
if not found then
|
||||
select image_id into v_image_id
|
||||
from image
|
||||
where original_hash = p_original_hash;
|
||||
end if;
|
||||
end if;
|
||||
|
||||
-- Insert image version
|
||||
insert into image_version (image_id, version, data)
|
||||
select image_id, p_version, p_data from image where original_hash = p_original_hash
|
||||
on conflict do nothing
|
||||
returning image_id into v_image_id;
|
||||
if not found then
|
||||
select image_id into v_image_id
|
||||
from image i
|
||||
join image_version iv using (image_id)
|
||||
where i.original_hash = p_original_hash
|
||||
and iv.version = p_version;
|
||||
end if;
|
||||
|
||||
return query select v_image_id;
|
||||
end
|
||||
$$ language plpgsql;
|
||||
|
|
@ -18,7 +18,7 @@ begin
|
|||
display_name,
|
||||
description,
|
||||
home_url,
|
||||
logo_url,
|
||||
image_id,
|
||||
keywords,
|
||||
latest_version,
|
||||
package_kind_id,
|
||||
|
|
@ -28,7 +28,7 @@ begin
|
|||
nullif(p_pkg->>'display_name', ''),
|
||||
nullif(p_pkg->>'description', ''),
|
||||
nullif(p_pkg->>'home_url', ''),
|
||||
nullif(p_pkg->>'logo_url', ''),
|
||||
nullif(p_pkg->>'image_id', '')::uuid,
|
||||
(select (array(select jsonb_array_elements_text(nullif(p_pkg->'keywords', 'null'::jsonb))))::text[]),
|
||||
p_pkg->>'version',
|
||||
(p_pkg->>'kind')::int,
|
||||
|
|
@ -40,6 +40,7 @@ begin
|
|||
display_name = excluded.display_name,
|
||||
description = excluded.description,
|
||||
home_url = excluded.home_url,
|
||||
image_id = excluded.image_id,
|
||||
keywords = excluded.keywords,
|
||||
latest_version = excluded.latest_version,
|
||||
updated_at = current_timestamp
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ begin
|
|||
p.name,
|
||||
p.display_name,
|
||||
p.description,
|
||||
p.logo_url,
|
||||
p.image_id,
|
||||
s.app_version,
|
||||
r.chart_repository_id as chart_repository_id,
|
||||
r.name as chart_repository_name,
|
||||
|
|
@ -54,7 +54,7 @@ begin
|
|||
'name', name,
|
||||
'display_name', display_name,
|
||||
'description', description,
|
||||
'logo_url', logo_url,
|
||||
'image_id', image_id,
|
||||
'app_version', app_version,
|
||||
'chart_repository', (
|
||||
select json_build_object(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ select plan(2);
|
|||
\set repo1ID '00000000-0000-0000-0000-000000000001'
|
||||
\set package1ID '00000000-0000-0000-0000-000000000001'
|
||||
\set package2ID '00000000-0000-0000-0000-000000000002'
|
||||
\set image1ID '00000000-0000-0000-0000-000000000001'
|
||||
\set image2ID '00000000-0000-0000-0000-000000000002'
|
||||
|
||||
-- No packages at this point
|
||||
select is(
|
||||
|
|
@ -23,7 +25,7 @@ insert into package (
|
|||
display_name,
|
||||
description,
|
||||
home_url,
|
||||
logo_url,
|
||||
image_id,
|
||||
keywords,
|
||||
latest_version,
|
||||
package_kind_id,
|
||||
|
|
@ -34,7 +36,7 @@ insert into package (
|
|||
'Package 1',
|
||||
'description',
|
||||
'home_url',
|
||||
'logo_url',
|
||||
:'image1ID',
|
||||
'{"kw1", "kw2"}',
|
||||
'1.0.0',
|
||||
0,
|
||||
|
|
@ -76,7 +78,7 @@ insert into package (
|
|||
display_name,
|
||||
description,
|
||||
home_url,
|
||||
logo_url,
|
||||
image_id,
|
||||
keywords,
|
||||
latest_version,
|
||||
package_kind_id,
|
||||
|
|
@ -87,7 +89,7 @@ insert into package (
|
|||
'Package 2',
|
||||
'description',
|
||||
'home_url',
|
||||
'logo_url',
|
||||
:'image2ID',
|
||||
'{"kw1", "kw2"}',
|
||||
'1.0.0',
|
||||
0,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
-- Start transaction and plan tests
|
||||
begin;
|
||||
select plan(8);
|
||||
|
||||
-- Try getting a non existent image
|
||||
select is_empty(
|
||||
$$ select get_image('00000000-0000-0000-0000-000000000001'::uuid, ''); $$,
|
||||
'Image1 does not exist, nothing should be returned'
|
||||
);
|
||||
|
||||
-- Register 2x version of image1
|
||||
insert into image (image_id, original_hash)
|
||||
values ('00000000-0000-0000-0000-000000000001'::uuid, 'image1Hash'::bytea);
|
||||
insert into image_version (image_id, version, data)
|
||||
values ('00000000-0000-0000-0000-000000000001'::uuid, '2x', 'image12xData'::bytea);
|
||||
|
||||
-- Get image version data just registered
|
||||
select results_eq(
|
||||
$$ select get_image('00000000-0000-0000-0000-000000000001', '2x') $$,
|
||||
$$ values ('image12xData'::bytea) $$,
|
||||
'image1 version 2x data should be returned'
|
||||
);
|
||||
|
||||
-- Register 1x version of image1
|
||||
insert into image_version (image_id, version, data)
|
||||
values ('00000000-0000-0000-0000-000000000001'::uuid, '1x', 'image11xData'::bytea);
|
||||
|
||||
-- We have two versions of image1 registered now
|
||||
select results_eq(
|
||||
$$ select get_image('00000000-0000-0000-0000-000000000001', '1x') $$,
|
||||
$$ values ('image11xData'::bytea) $$,
|
||||
'image1 version 1x data should be returned when requesting it'
|
||||
);
|
||||
select results_eq(
|
||||
$$ select get_image('00000000-0000-0000-0000-000000000001', '') $$,
|
||||
$$ values ('image11xData'::bytea) $$,
|
||||
'image1 version 1x data should be returned when requesting no specific version'
|
||||
);
|
||||
select results_eq(
|
||||
$$ select get_image('00000000-0000-0000-0000-000000000001', '20x') $$,
|
||||
$$ values ('image11xData'::bytea) $$,
|
||||
'image1 version 1x data should be returned when requesting a non existent version'
|
||||
);
|
||||
|
||||
-- Register image2 (svg)
|
||||
insert into image (image_id, original_hash)
|
||||
values ('00000000-0000-0000-0000-000000000002'::uuid, 'image2Hash'::bytea);
|
||||
insert into image_version (image_id, version, data)
|
||||
values ('00000000-0000-0000-0000-000000000002'::uuid, 'svg', 'image2SvgData'::bytea);
|
||||
|
||||
-- Check image was registered
|
||||
select results_eq(
|
||||
$$ select get_image('00000000-0000-0000-0000-000000000002', 'svg') $$,
|
||||
$$ values ('image2SvgData'::bytea) $$,
|
||||
'image2 svg data should be returned when requesting it'
|
||||
);
|
||||
select results_eq(
|
||||
$$ select get_image('00000000-0000-0000-0000-000000000002', '') $$,
|
||||
$$ values ('image2SvgData'::bytea) $$,
|
||||
'image2 svg data should be returned when requesting no specific version'
|
||||
);
|
||||
select results_eq(
|
||||
$$ select get_image('00000000-0000-0000-0000-000000000002', '2x') $$,
|
||||
$$ values ('image2SvgData'::bytea) $$,
|
||||
'image2 svg data should be returned when requesting a non existent version'
|
||||
);
|
||||
|
||||
-- Finish tests and rollback transaction
|
||||
select * from finish();
|
||||
rollback;
|
||||
|
|
@ -7,6 +7,7 @@ select plan(2);
|
|||
\set package1ID '00000000-0000-0000-0000-000000000001'
|
||||
\set maintainer1ID '00000000-0000-0000-0000-000000000001'
|
||||
\set maintainer2ID '00000000-0000-0000-0000-000000000002'
|
||||
\set image1ID '00000000-0000-0000-0000-000000000001'
|
||||
|
||||
-- No packages at this point
|
||||
select is_empty(
|
||||
|
|
@ -27,7 +28,7 @@ insert into package (
|
|||
display_name,
|
||||
description,
|
||||
home_url,
|
||||
logo_url,
|
||||
image_id,
|
||||
keywords,
|
||||
latest_version,
|
||||
package_kind_id,
|
||||
|
|
@ -38,7 +39,7 @@ insert into package (
|
|||
'Package 1',
|
||||
'description',
|
||||
'home_url',
|
||||
'logo_url',
|
||||
:'image1ID',
|
||||
'{"kw1", "kw2"}',
|
||||
'1.0.0',
|
||||
0,
|
||||
|
|
@ -89,7 +90,7 @@ select is(
|
|||
"display_name": "Package 1",
|
||||
"description": "description",
|
||||
"home_url": "home_url",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000001",
|
||||
"keywords": ["kw1", "kw2"],
|
||||
"readme": "readme-version-1.0.0",
|
||||
"links": {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ select plan(3);
|
|||
\set package1ID '00000000-0000-0000-0000-000000000001'
|
||||
\set maintainer1ID '00000000-0000-0000-0000-000000000001'
|
||||
\set maintainer2ID '00000000-0000-0000-0000-000000000002'
|
||||
\set image1ID '00000000-0000-0000-0000-000000000001'
|
||||
|
||||
-- No packages at this point
|
||||
select is_empty(
|
||||
|
|
@ -27,7 +28,7 @@ insert into package (
|
|||
display_name,
|
||||
description,
|
||||
home_url,
|
||||
logo_url,
|
||||
image_id,
|
||||
keywords,
|
||||
latest_version,
|
||||
package_kind_id,
|
||||
|
|
@ -38,7 +39,7 @@ insert into package (
|
|||
'Package 1',
|
||||
'description',
|
||||
'home_url',
|
||||
'logo_url',
|
||||
:'image1ID',
|
||||
'{"kw1", "kw2"}',
|
||||
'1.0.0',
|
||||
0,
|
||||
|
|
@ -89,7 +90,7 @@ select is(
|
|||
"display_name": "Package 1",
|
||||
"description": "description",
|
||||
"home_url": "home_url",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000001",
|
||||
"keywords": ["kw1", "kw2"],
|
||||
"readme": "readme-version-1.0.0",
|
||||
"links": {
|
||||
|
|
@ -127,7 +128,7 @@ select is(
|
|||
"display_name": "Package 1",
|
||||
"description": "description",
|
||||
"home_url": "home_url",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000001",
|
||||
"keywords": ["kw1", "kw2"],
|
||||
"readme": "readme-version-0.0.9",
|
||||
"links": {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ select plan(3);
|
|||
\set repo2ID '00000000-0000-0000-0000-000000000002'
|
||||
\set package1ID '00000000-0000-0000-0000-000000000001'
|
||||
\set package2ID '00000000-0000-0000-0000-000000000002'
|
||||
\set image1ID '00000000-0000-0000-0000-000000000001'
|
||||
\set image2ID '00000000-0000-0000-0000-000000000002'
|
||||
|
||||
-- No packages at this point
|
||||
select is(
|
||||
|
|
@ -29,7 +31,7 @@ insert into package (
|
|||
display_name,
|
||||
description,
|
||||
home_url,
|
||||
logo_url,
|
||||
image_id,
|
||||
keywords,
|
||||
latest_version,
|
||||
created_at,
|
||||
|
|
@ -42,7 +44,7 @@ insert into package (
|
|||
'Package 1',
|
||||
'description',
|
||||
'home_url',
|
||||
'logo_url',
|
||||
:'image1ID',
|
||||
'{"kw1", "kw2"}',
|
||||
'1.0.0',
|
||||
current_timestamp - '1s'::interval,
|
||||
|
|
@ -71,7 +73,7 @@ insert into package (
|
|||
display_name,
|
||||
description,
|
||||
home_url,
|
||||
logo_url,
|
||||
image_id,
|
||||
keywords,
|
||||
latest_version,
|
||||
created_at,
|
||||
|
|
@ -84,7 +86,7 @@ insert into package (
|
|||
'Package 2',
|
||||
'description',
|
||||
'home_url',
|
||||
'logo_url',
|
||||
:'image2ID',
|
||||
'{"kw1", "kw2"}',
|
||||
'1.0.0',
|
||||
current_timestamp - '2s'::interval,
|
||||
|
|
@ -117,7 +119,7 @@ select is(
|
|||
"kind": 0,
|
||||
"name": "package1",
|
||||
"display_name": "Package 1",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000001",
|
||||
"app_version": "12.1.0",
|
||||
"chart_repository": {
|
||||
"name": "repo1",
|
||||
|
|
@ -128,7 +130,7 @@ select is(
|
|||
"kind": 0,
|
||||
"name": "package2",
|
||||
"display_name": "Package 2",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000002",
|
||||
"app_version": "12.1.0",
|
||||
"chart_repository": {
|
||||
"name": "repo2",
|
||||
|
|
@ -140,7 +142,7 @@ select is(
|
|||
"kind": 0,
|
||||
"name": "package1",
|
||||
"display_name": "Package 1",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000001",
|
||||
"app_version": "12.1.0",
|
||||
"chart_repository": {
|
||||
"name": "repo1",
|
||||
|
|
@ -151,7 +153,7 @@ select is(
|
|||
"kind": 0,
|
||||
"name": "package2",
|
||||
"display_name": "Package 2",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000002",
|
||||
"app_version": "12.1.0",
|
||||
"chart_repository": {
|
||||
"name": "repo2",
|
||||
|
|
@ -170,7 +172,7 @@ select register_package('
|
|||
"display_name": "Package 2 v2",
|
||||
"description": "description v2",
|
||||
"home_url": "home_url",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000002",
|
||||
"keywords": ["kw1", "kw2"],
|
||||
"readme": "readme-version-2.0.0",
|
||||
"version": "2.0.0",
|
||||
|
|
@ -197,7 +199,7 @@ select is(
|
|||
"kind": 0,
|
||||
"name": "package1",
|
||||
"display_name": "Package 1",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000001",
|
||||
"app_version": "12.1.0",
|
||||
"chart_repository": {
|
||||
"name": "repo1",
|
||||
|
|
@ -208,7 +210,7 @@ select is(
|
|||
"kind": 0,
|
||||
"name": "package2",
|
||||
"display_name": "Package 2 v2",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000002",
|
||||
"app_version": "13.0.0",
|
||||
"chart_repository": {
|
||||
"name": "repo2",
|
||||
|
|
@ -220,7 +222,7 @@ select is(
|
|||
"kind": 0,
|
||||
"name": "package2",
|
||||
"display_name": "Package 2 v2",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000002",
|
||||
"app_version": "13.0.0",
|
||||
"chart_repository": {
|
||||
"name": "repo2",
|
||||
|
|
@ -231,7 +233,7 @@ select is(
|
|||
"kind": 0,
|
||||
"name": "package1",
|
||||
"display_name": "Package 1",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000001",
|
||||
"app_version": "12.1.0",
|
||||
"chart_repository": {
|
||||
"name": "repo1",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ select plan(2);
|
|||
\set repo1ID '00000000-0000-0000-0000-000000000001'
|
||||
\set package1ID '00000000-0000-0000-0000-000000000001'
|
||||
\set package2ID '00000000-0000-0000-0000-000000000002'
|
||||
\set image1ID '00000000-0000-0000-0000-000000000001'
|
||||
\set image2ID '00000000-0000-0000-0000-000000000002'
|
||||
|
||||
-- No packages at this point
|
||||
select is(
|
||||
|
|
@ -26,7 +28,7 @@ insert into package (
|
|||
display_name,
|
||||
description,
|
||||
home_url,
|
||||
logo_url,
|
||||
image_id,
|
||||
keywords,
|
||||
latest_version,
|
||||
package_kind_id,
|
||||
|
|
@ -37,7 +39,7 @@ insert into package (
|
|||
'Package 1',
|
||||
'description',
|
||||
'home_url',
|
||||
'logo_url',
|
||||
:'image1ID',
|
||||
'{"kw1", "kw2"}',
|
||||
'1.0.0',
|
||||
0,
|
||||
|
|
@ -79,7 +81,7 @@ insert into package (
|
|||
display_name,
|
||||
description,
|
||||
home_url,
|
||||
logo_url,
|
||||
image_id,
|
||||
keywords,
|
||||
latest_version,
|
||||
package_kind_id,
|
||||
|
|
@ -90,7 +92,7 @@ insert into package (
|
|||
'Package 2',
|
||||
'description',
|
||||
'home_url',
|
||||
'logo_url',
|
||||
:'image2ID',
|
||||
'{"kw1", "kw2"}',
|
||||
'1.0.0',
|
||||
0,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
-- Start transaction and plan tests
|
||||
begin;
|
||||
select plan(10);
|
||||
|
||||
-- Try registering an image version with empty version
|
||||
select throws_ok(
|
||||
$$ select register_image('image1Hash'::bytea, '', 'image1Data'::bytea); $$,
|
||||
23514,
|
||||
'new row for relation "image_version" violates check constraint "image_version_version_check"',
|
||||
'A non empty version is required to register an image'
|
||||
);
|
||||
|
||||
-- Register image1 version 2x
|
||||
select register_image('image1Hash'::bytea, '2x', 'image12xData'::bytea);
|
||||
|
||||
-- Image1 2x has been registered
|
||||
select results_eq(
|
||||
'select original_hash from image',
|
||||
$$ values ('image1Hash'::bytea) $$,
|
||||
'image1 parent image should be registered'
|
||||
);
|
||||
select results_eq(
|
||||
'select version, data from image_version',
|
||||
$$ values ('2x', 'image12xData'::bytea) $$,
|
||||
'image1 version 2x should be registered'
|
||||
);
|
||||
select lives_ok(
|
||||
$$ select register_image('image1Hash'::bytea, '2x', 'image12xData'::bytea) $$,
|
||||
'Registering again image1 2x is ok, it will be a noop'
|
||||
);
|
||||
select lives_ok(
|
||||
$$ select register_image('image1Hash'::bytea, '2x', 'image12xNewData'::bytea) $$,
|
||||
'Registering again image1 2x with different data is ok, it will be a noop'
|
||||
);
|
||||
select results_eq(
|
||||
'select version, data from image_version',
|
||||
$$ values ('2x', 'image12xData'::bytea) $$,
|
||||
'image1 version 2x has not changed after the previous insert operation'
|
||||
);
|
||||
|
||||
-- Register image1 version 4x
|
||||
select register_image('image1Hash'::bytea, '4x', 'image14xData'::bytea);
|
||||
|
||||
-- Check new version was registered
|
||||
select results_eq(
|
||||
'select original_hash from image',
|
||||
$$ values ('image1Hash'::bytea) $$,
|
||||
'image1 should be the only parent image registered'
|
||||
);
|
||||
select results_eq(
|
||||
'select version, data from image_version',
|
||||
$$ values
|
||||
('2x', 'image12xData'::bytea),
|
||||
('4x', 'image14xData'::bytea)
|
||||
$$,
|
||||
'Image1 versions 2x and 4x found'
|
||||
);
|
||||
|
||||
-- Register now image2 version 3x
|
||||
select register_image('image2Hash'::bytea, '3x', 'image23xData'::bytea);
|
||||
|
||||
-- Check new image was registered
|
||||
select results_eq(
|
||||
'select original_hash from image',
|
||||
$$ values
|
||||
('image1Hash'::bytea),
|
||||
('image2Hash'::bytea)
|
||||
$$,
|
||||
'Two parent images expected now, image1 and image2'
|
||||
);
|
||||
select results_eq(
|
||||
'select version, data from image_version',
|
||||
$$ values
|
||||
('2x', 'image12xData'::bytea),
|
||||
('4x', 'image14xData'::bytea),
|
||||
('3x', 'image23xData'::bytea)
|
||||
$$,
|
||||
'Three versions from two different images found'
|
||||
);
|
||||
|
||||
-- Finish tests and rollback transaction
|
||||
select * from finish();
|
||||
rollback;
|
||||
|
|
@ -17,7 +17,7 @@ select register_package('
|
|||
"display_name": "Package 1",
|
||||
"description": "description",
|
||||
"home_url": "home_url",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000001",
|
||||
"keywords": ["kw1", "kw2"],
|
||||
"readme": "readme-version-1.0.0",
|
||||
"links": {
|
||||
|
|
@ -51,7 +51,7 @@ select results_eq(
|
|||
display_name,
|
||||
description,
|
||||
home_url,
|
||||
logo_url,
|
||||
image_id,
|
||||
keywords,
|
||||
latest_version,
|
||||
package_kind_id,
|
||||
|
|
@ -65,7 +65,7 @@ select results_eq(
|
|||
'Package 1',
|
||||
'description',
|
||||
'home_url',
|
||||
'logo_url',
|
||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
||||
'{kw1,kw2}'::text[],
|
||||
'1.0.0',
|
||||
0,
|
||||
|
|
@ -125,7 +125,7 @@ select register_package('
|
|||
"display_name": "Package 1 v2",
|
||||
"description": "description v2",
|
||||
"home_url": "home_url",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000001",
|
||||
"keywords": ["kw1", "kw2"],
|
||||
"readme": "readme-version-2.0.0",
|
||||
"version": "2.0.0",
|
||||
|
|
@ -206,7 +206,7 @@ select register_package('
|
|||
"display_name": "Package 1",
|
||||
"description": "description",
|
||||
"home_url": "home_url",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000001",
|
||||
"keywords": ["kw1", "kw2"],
|
||||
"readme": "readme-version-0.0.9",
|
||||
"version": "0.0.9",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ select plan(9);
|
|||
\set repo2ID '00000000-0000-0000-0000-000000000002'
|
||||
\set package1ID '00000000-0000-0000-0000-000000000001'
|
||||
\set package2ID '00000000-0000-0000-0000-000000000002'
|
||||
\set image1ID '00000000-0000-0000-0000-000000000001'
|
||||
\set image2ID '00000000-0000-0000-0000-000000000002'
|
||||
|
||||
-- Using invalid queries
|
||||
select throws_ok(
|
||||
|
|
@ -43,7 +45,7 @@ insert into package (
|
|||
display_name,
|
||||
description,
|
||||
home_url,
|
||||
logo_url,
|
||||
image_id,
|
||||
keywords,
|
||||
latest_version,
|
||||
package_kind_id,
|
||||
|
|
@ -54,7 +56,7 @@ insert into package (
|
|||
'Package 1',
|
||||
'description',
|
||||
'home_url',
|
||||
'logo_url',
|
||||
:'image1ID',
|
||||
'{"kw1", "kw2"}',
|
||||
'1.0.0',
|
||||
0,
|
||||
|
|
@ -96,7 +98,7 @@ insert into package (
|
|||
display_name,
|
||||
description,
|
||||
home_url,
|
||||
logo_url,
|
||||
image_id,
|
||||
keywords,
|
||||
latest_version,
|
||||
package_kind_id,
|
||||
|
|
@ -107,7 +109,7 @@ insert into package (
|
|||
'Package 2',
|
||||
'description',
|
||||
'home_url',
|
||||
'logo_url',
|
||||
:'image2ID',
|
||||
'{"kw1", "kw2"}',
|
||||
'1.0.0',
|
||||
0,
|
||||
|
|
@ -154,7 +156,7 @@ select is(
|
|||
"packages": [{
|
||||
"kind": 0,
|
||||
"name": "package1",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000001",
|
||||
"package_id": "00000000-0000-0000-0000-000000000001",
|
||||
"app_version": "12.1.0",
|
||||
"description": "description",
|
||||
|
|
@ -166,7 +168,7 @@ select is(
|
|||
}, {
|
||||
"kind": 0,
|
||||
"name": "package2",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000002",
|
||||
"package_id": "00000000-0000-0000-0000-000000000002",
|
||||
"app_version": "12.1.0",
|
||||
"description": "description",
|
||||
|
|
@ -209,7 +211,7 @@ select is(
|
|||
"packages": [{
|
||||
"kind": 0,
|
||||
"name": "package1",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000001",
|
||||
"package_id": "00000000-0000-0000-0000-000000000001",
|
||||
"app_version": "12.1.0",
|
||||
"description": "description",
|
||||
|
|
@ -252,7 +254,7 @@ select is(
|
|||
"packages": [{
|
||||
"kind": 0,
|
||||
"name": "package2",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "00000000-0000-0000-0000-000000000002",
|
||||
"package_id": "00000000-0000-0000-0000-000000000002",
|
||||
"app_version": "12.1.0",
|
||||
"description": "description",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
-- Start transaction and plan tests
|
||||
begin;
|
||||
select plan(28);
|
||||
select plan(33);
|
||||
|
||||
-- Check default_text_search_config is correct
|
||||
select results_eq(
|
||||
|
|
@ -15,6 +15,8 @@ select has_extension('uuid-ossp');
|
|||
-- Check expected tables exist
|
||||
select tables_are(array[
|
||||
'chart_repository',
|
||||
'image',
|
||||
'image_version',
|
||||
'maintainer',
|
||||
'package',
|
||||
'package__maintainer',
|
||||
|
|
@ -30,6 +32,21 @@ select columns_are('chart_repository', array[
|
|||
'display_name',
|
||||
'url'
|
||||
]);
|
||||
select columns_are('chart_repository', array[
|
||||
'chart_repository_id',
|
||||
'name',
|
||||
'display_name',
|
||||
'url'
|
||||
]);
|
||||
select columns_are('image', array[
|
||||
'image_id',
|
||||
'original_hash'
|
||||
]);
|
||||
select columns_are('image_version', array[
|
||||
'image_id',
|
||||
'version',
|
||||
'data'
|
||||
]);
|
||||
select columns_are('maintainer', array[
|
||||
'maintainer_id',
|
||||
'name',
|
||||
|
|
@ -41,7 +58,7 @@ select columns_are('package', array[
|
|||
'display_name',
|
||||
'description',
|
||||
'home_url',
|
||||
'logo_url',
|
||||
'image_id',
|
||||
'keywords',
|
||||
'latest_version',
|
||||
'created_at',
|
||||
|
|
@ -111,9 +128,11 @@ select has_function('search_packages');
|
|||
select has_function('get_package_version');
|
||||
select has_function('get_package');
|
||||
select has_function('get_packages_updates');
|
||||
select has_function('register_image');
|
||||
select has_function('get_image');
|
||||
|
||||
-- Check package kinds exist
|
||||
SELECT results_eq(
|
||||
select results_eq(
|
||||
'select * from package_kind',
|
||||
$$ values (0, 'chart') $$,
|
||||
'Package kinds should exist'
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -3,6 +3,8 @@ module github.com/tegioz/hub
|
|||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c
|
||||
github.com/jackc/pgconn v1.2.1
|
||||
github.com/jackc/pgproto3 v1.1.0
|
||||
github.com/jackc/pgx/v4 v4.2.1
|
||||
|
|
|
|||
17
go.sum
17
go.sum
|
|
@ -57,7 +57,9 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
|||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/containerd/containerd v1.3.0-beta.2.0.20190823190603-4a2f61c4f2b4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/containerd v1.3.0 h1:xjvXQWABwS2uiv3TWgQt5Uth60Gu86LTGZXMJkjc7rY=
|
||||
github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 h1:4BX8f882bXEDKfWIf0wa8HRvpnBoPszJJXL+TVbBw4M=
|
||||
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||
github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
|
|
@ -83,13 +85,18 @@ github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1
|
|||
github.com/deislabs/oras v0.7.0/go.mod h1:sqMKPG3tMyIX9xwXUBRLhZ24o+uT4y6jgBD2RzUTKDM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/docker/cli v0.0.0-20190506213505-d88565df0c2d/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker-credential-helpers v0.6.1/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||
|
|
@ -224,6 +231,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmg
|
|||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.3.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c h1:fEE5/5VNnYUoBOj2I9TP8Jc+a7lge3QWn9DKE7NCwfc=
|
||||
github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c/go.mod h1:ObS/W+h8RYb1Y7fYivughjxojTmIu5iAIjSrSLCLeqE=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk=
|
||||
|
|
@ -325,6 +334,7 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
|||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-shellwords v1.0.5 h1:JhhFTIOslh5ZsPrpa3Wdg8bF0WI3b44EMblmU9wIsXc=
|
||||
github.com/mattn/go-shellwords v1.0.5/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v0.0.0-20181005163659-0d29b283ac0f/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
|
|
@ -358,8 +368,11 @@ github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGV
|
|||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
|
||||
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
|
|
@ -492,6 +505,8 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL
|
|||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
|
|
@ -598,6 +613,7 @@ google.golang.org/genproto v0.0.0-20190128161407-8ac453e89fca/go.mod h1:L3J43x8/
|
|||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6 h1:UXl+Zk3jqqcbEVV7ace5lrt4YdA4tXiz3f/KbmD29Vo=
|
||||
google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
|
|
@ -605,6 +621,7 @@ google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiq
|
|||
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s=
|
||||
google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ func TestSearchPackagesJSON(t *testing.T) {
|
|||
"packages": [{
|
||||
"kind": 0,
|
||||
"name": "package1",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "image_id",
|
||||
"package_id": "00000000-0000-0000-0000-000000000001",
|
||||
"app_version": "12.1.0",
|
||||
"description": "description",
|
||||
|
|
@ -158,7 +158,7 @@ func TestSearchPackagesJSON(t *testing.T) {
|
|||
}, {
|
||||
"kind": 0,
|
||||
"name": "package2",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "image_id",
|
||||
"package_id": "00000000-0000-0000-0000-000000000002",
|
||||
"app_version": "12.1.0",
|
||||
"description": "description",
|
||||
|
|
@ -207,7 +207,7 @@ func TestRegisterPackage(t *testing.T) {
|
|||
Name: "package1",
|
||||
Description: "description",
|
||||
HomeURL: "home_url",
|
||||
LogoURL: "logo_url",
|
||||
ImageID: "image_id",
|
||||
Keywords: []string{"kw1", "kw2"},
|
||||
Readme: "readme-version-1.0.0",
|
||||
Links: []*Link{
|
||||
|
|
@ -253,7 +253,7 @@ func TestGetPackageJSON(t *testing.T) {
|
|||
"display_name": "Package 1",
|
||||
"description": "description",
|
||||
"home_url": "home_url",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "image_id",
|
||||
"keywords": ["kw1", "kw2"],
|
||||
"readme": "readme-version-1.0.0",
|
||||
"links": {
|
||||
|
|
@ -300,7 +300,7 @@ func TestGetPackageVersionJSON(t *testing.T) {
|
|||
"display_name": "Package 1",
|
||||
"description": "description",
|
||||
"home_url": "home_url",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "image_id",
|
||||
"keywords": ["kw1", "kw2"],
|
||||
"readme": "readme-version-1.0.0",
|
||||
"links": {
|
||||
|
|
@ -346,7 +346,7 @@ func TestGetPackagesUpdatesJSON(t *testing.T) {
|
|||
"kind": 0,
|
||||
"name": "package1",
|
||||
"display_name": "Package 1",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "image_id",
|
||||
"app_version": "12.1.0",
|
||||
"chart_repository": {
|
||||
"name": "repo1",
|
||||
|
|
@ -357,7 +357,7 @@ func TestGetPackagesUpdatesJSON(t *testing.T) {
|
|||
"kind": 0,
|
||||
"name": "package2",
|
||||
"display_name": "Package 2 v2",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "image_id",
|
||||
"app_version": "13.0.0",
|
||||
"chart_repository": {
|
||||
"name": "repo2",
|
||||
|
|
@ -369,7 +369,7 @@ func TestGetPackagesUpdatesJSON(t *testing.T) {
|
|||
"kind": 0,
|
||||
"name": "package2",
|
||||
"display_name": "Package 2 v2",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "image_id",
|
||||
"app_version": "13.0.0",
|
||||
"chart_repository": {
|
||||
"name": "repo2",
|
||||
|
|
@ -380,7 +380,7 @@ func TestGetPackagesUpdatesJSON(t *testing.T) {
|
|||
"kind": 0,
|
||||
"name": "package1",
|
||||
"display_name": "Package 1",
|
||||
"logo_url": "logo_url",
|
||||
"image_id": "image_id",
|
||||
"app_version": "12.1.0",
|
||||
"chart_repository": {
|
||||
"name": "repo1",
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ type Package struct {
|
|||
DisplayName string `json:"display_name"`
|
||||
Description string `json:"description"`
|
||||
HomeURL string `json:"home_url"`
|
||||
LogoURL string `json:"logo_url"`
|
||||
ImageID string `json:"image_id"`
|
||||
Keywords []string `json:"keywords"`
|
||||
Readme string `json:"readme"`
|
||||
Links []*Link `json:"links"`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
package img
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
)
|
||||
|
||||
// Store describes the methods an image.Store implementation must provide.
|
||||
type Store interface {
|
||||
// SaveImage stores an image returning the image ID.
|
||||
SaveImage(ctx context.Context, data []byte) (imageID string, err error)
|
||||
}
|
||||
|
||||
// ImageVersion represents a specific size version of an image.
|
||||
type ImageVersion struct {
|
||||
Version string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// GenerateImageVersions generates multiple versions of different sizes for the
|
||||
// image provided.
|
||||
func GenerateImageVersions(data []byte) ([]*ImageVersion, error) {
|
||||
// Define versions spec
|
||||
spec := []struct {
|
||||
version string
|
||||
width int
|
||||
height int
|
||||
}{
|
||||
{"1x", 80, 80},
|
||||
{"2x", 160, 160},
|
||||
{"3x", 240, 240},
|
||||
{"4x", 320, 320},
|
||||
}
|
||||
|
||||
// Decode original image data
|
||||
img, err := imaging.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate image versions
|
||||
var imgVersions []*ImageVersion
|
||||
for _, e := range spec {
|
||||
imgVersion := imaging.Fit(img, e.width, e.height, imaging.Lanczos)
|
||||
var buf bytes.Buffer
|
||||
if err := imaging.Encode(&buf, imgVersion, imaging.PNG); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imgVersions = append(imgVersions, &ImageVersion{
|
||||
Version: e.version,
|
||||
Data: buf.Bytes(),
|
||||
})
|
||||
}
|
||||
|
||||
return imgVersions, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package pg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
|
||||
svg "github.com/h2non/go-is-svg"
|
||||
"github.com/jackc/pgx/v4"
|
||||
"github.com/tegioz/hub/internal/img"
|
||||
)
|
||||
|
||||
// DB defines the methods the database handler must provide.
|
||||
type DB interface {
|
||||
QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row
|
||||
}
|
||||
|
||||
// ImageStore is an image.Store implementation that uses PostgreSQL as the
|
||||
// underlying storage.
|
||||
type ImageStore struct {
|
||||
db DB
|
||||
}
|
||||
|
||||
// NewImageStore creates a new ImageStore instance.
|
||||
func NewImageStore(db DB) *ImageStore {
|
||||
return &ImageStore{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// SaveImage implements the image.Store interface.
|
||||
func (s *ImageStore) SaveImage(ctx context.Context, data []byte) (string, error) {
|
||||
// Compute image hash using sha256
|
||||
sum := sha256.Sum256(data)
|
||||
originalHash := sum[:]
|
||||
|
||||
// If image is already registered we just return its url
|
||||
imageID, err := s.getImageID(ctx, originalHash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if imageID != "" {
|
||||
return imageID, nil
|
||||
}
|
||||
|
||||
// If image format is svg register it as is in database, as this format
|
||||
// doesn't require to store additional size specific versions
|
||||
if svg.Is(data) {
|
||||
return s.registerImage(ctx, originalHash, data, "svg")
|
||||
}
|
||||
|
||||
// Generate image versions of different sizes and store them
|
||||
imageVersions, err := img.GenerateImageVersions(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, v := range imageVersions {
|
||||
imageID, err = s.registerImage(ctx, originalHash, v.Data, v.Version)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return imageID, nil
|
||||
}
|
||||
|
||||
// getImageID checks if the database contains an image with the hash provided,
|
||||
// returning its id when found.
|
||||
func (s *ImageStore) getImageID(ctx context.Context, hash []byte) (string, error) {
|
||||
var imageID string
|
||||
query := "select image_id from image where original_hash = $1"
|
||||
err := s.db.QueryRow(ctx, query, hash).Scan(&imageID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return imageID, nil
|
||||
}
|
||||
|
||||
// registerImage stores the image provided in the database.
|
||||
func (s *ImageStore) registerImage(ctx context.Context, hash []byte, data []byte, version string) (string, error) {
|
||||
var imageID string
|
||||
query := "select register_image($1, $2, $3)"
|
||||
err := s.db.QueryRow(ctx, query, hash, version, data).Scan(&imageID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return imageID, nil
|
||||
}
|
||||
|
||||
// GetImage returns an image stored in the database.
|
||||
func (s *ImageStore) GetImage(ctx context.Context, imageID, version string) ([]byte, error) {
|
||||
var data []byte
|
||||
query := "select get_image($1, $2)"
|
||||
err := s.db.QueryRow(ctx, query, imageID, version).Scan(&data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/tegioz/hub/internal/img"
|
||||
"github.com/tegioz/hub/internal/img/pg"
|
||||
)
|
||||
|
||||
// SetupImageStore creates a new image store based on the configuration provided.
|
||||
func SetupImageStore(cfg *viper.Viper, db pg.DB) (img.Store, error) {
|
||||
imageStore := cfg.GetString("tracker.imageStore")
|
||||
switch imageStore {
|
||||
case "pg":
|
||||
return pg.NewImageStore(db), nil
|
||||
default:
|
||||
return nil, errors.New("invalid image store")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
import { SearchResults, PackageDetail, Stats, Filters, PackagesUpdatesInfo } from '../types';
|
||||
import fetchApi from '../utils/fetchApi';
|
||||
import prepareFiltersQuery from '../utils/prepareFiltersQuery';
|
||||
import getEndpointPrefix from '../utils/getEndpointPrefix';
|
||||
|
||||
let API_ROUTE = '/api/v1';
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
API_ROUTE = `${process.env.REACT_APP_API_ENDPOINT}${API_ROUTE}`;
|
||||
}
|
||||
const API_ROUTE = `${getEndpointPrefix()}/api/v1`;
|
||||
|
||||
const API = {
|
||||
getPackage: (id?: string, version?: string): Promise<PackageDetail> => {
|
||||
|
|
|
|||
|
|
@ -1,23 +1,39 @@
|
|||
import React, { useState } from 'react';
|
||||
import isNull from 'lodash/isNull';
|
||||
import placeholder from '../../images/kubernetes_grey.svg';
|
||||
import getEndpointPrefix from '../../utils/getEndpointPrefix';
|
||||
|
||||
interface Props {
|
||||
src: string | null;
|
||||
imageId: string | null;
|
||||
alt: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Image = (props: Props) => {
|
||||
const [imageUrl, setImageUrl] = useState(props.src);
|
||||
const [error, setError] = useState(false);
|
||||
const src = isNull(props.imageId) ? '' : `${getEndpointPrefix()}/image/${props.imageId}`;
|
||||
|
||||
console.log(props);
|
||||
|
||||
return (
|
||||
<img
|
||||
alt={props.alt}
|
||||
src={isNull(imageUrl) ? placeholder : `${imageUrl}?raw=true`}
|
||||
className={props.className}
|
||||
onError={() => setImageUrl(placeholder)}
|
||||
/>
|
||||
<>
|
||||
{error || isNull(props.imageId) ? (
|
||||
<img
|
||||
alt={props.alt}
|
||||
src={placeholder}
|
||||
className={props.className}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
alt={props.alt}
|
||||
srcSet={`${src}@1x 1x, ${src}@2x 2x, ${src}@3x 3x, ${src}@4x 4x`}
|
||||
src={src}
|
||||
className={props.className}
|
||||
onError={() => setError(true)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React from 'react';
|
||||
import Image from './Image';
|
||||
import chartIcon from '../../images/helm.svg';
|
||||
import operatorIcon from '../../images/operator.svg';
|
||||
import { PackageKind } from '../../types';
|
||||
|
|
@ -15,7 +14,7 @@ const ICONS = {
|
|||
};
|
||||
|
||||
const PackageIcon = (props: Props) => (
|
||||
<Image
|
||||
<img
|
||||
alt="Icon"
|
||||
src={ICONS[props.kind]}
|
||||
className={props.className}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const UpdatesCard = (props: Props) => (
|
|||
<div className="d-flex align-items-start justify-content-between flex-grow-1">
|
||||
<div className={`d-flex align-items-center flex-grow-1 ${styles.truncateWrapper}`}>
|
||||
<div className={`d-flex align-items-center justify-content-center overflow-hidden p-1 ${styles.imageWrapper}`}>
|
||||
<Image src={props.packageItem.logo_url} alt={`Logo ${props.packageItem.display_name}`} className={styles.image} />
|
||||
<Image imageId={props.packageItem.image_id} alt={`Logo ${props.packageItem.display_name || props.packageItem.name}`} className={styles.image} />
|
||||
</div>
|
||||
|
||||
<div className={`ml-3 flex-grow-1 ${styles.truncateWrapper}`}>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ interface Props {
|
|||
const ModalHeader = (props: Props) => (
|
||||
<div className="d-flex align-items-center">
|
||||
<div className={`d-flex align-items-center justify-content-center p-1 overflow-hidden ${styles.imageWrapper}`}>
|
||||
<Image className={styles.image} alt={props.package.name} src={props.package.logo_url} />
|
||||
<Image className={styles.image} alt={props.package.display_name || props.package.name} imageId={props.package.image_id} />
|
||||
</div>
|
||||
|
||||
<div className="ml-3">
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const Title = (props: Props) => (
|
|||
<div className="container">
|
||||
<div className="d-flex align-items-center mb-3">
|
||||
<div className={`d-flex align-items-center justify-content-center p-1 overflow-hidden ${styles.imageWrapper}`}>
|
||||
<Image className={styles.image} alt={props.package.name} src={props.package.logo_url} />
|
||||
<Image className={styles.image} alt={props.package.display_name || props.package.name} imageId={props.package.image_id} />
|
||||
</div>
|
||||
|
||||
<div className="ml-3">
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const Card = (props: Props) => (
|
|||
<div className="d-flex align-items-start justify-content-between mb-3">
|
||||
<div className={`d-flex align-items-center flex-grow-1 ${styles.truncateWrapper}`}>
|
||||
<div className={`d-flex align-items-center justify-content-center overflow-hidden p-1 ${styles.imageWrapper}`}>
|
||||
<Image src={props.package.logo_url} alt={`Logo ${props.package.display_name}`} className={styles.image} />
|
||||
<Image imageId={props.package.image_id} alt={`Logo ${props.package.display_name || props.package.name}`} className={styles.image} />
|
||||
</div>
|
||||
|
||||
<div className={`ml-3 flex-grow-1 ${styles.truncateWrapper}`}>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export interface Package {
|
|||
name: string;
|
||||
display_name: string | null;
|
||||
description: string;
|
||||
logo_url: string | null;
|
||||
image_id: string | null;
|
||||
app_version: string;
|
||||
chart_repository: ChartRepository;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
export default (): string => {
|
||||
let endpoint = '';
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
endpoint = `${process.env.REACT_APP_API_ENDPOINT}`;
|
||||
}
|
||||
|
||||
return endpoint;
|
||||
}
|
||||
Loading…
Reference in New Issue