Store and serve packages logos (#93)

Closes #26

Signed-off-by: Sergio Castaño Arteaga <tegioz@icloud.com>
This commit is contained in:
Sergio C. Arteaga 2020-02-12 11:43:50 +01:00 committed by GitHub
parent 568b2acec8
commit accd31b3ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 724 additions and 121 deletions

View File

@ -17,3 +17,4 @@ stringData:
tracker:
numWorkers: {{ .Values.chartTracker.numWorkers }}
repositoriesNames: {{ .Values.chartTracker.repositories }}
imageStore: {{ .Values.chartTracker.imageStore }}

View File

@ -42,6 +42,7 @@ chartTracker:
repository: tegioz/chart-tracker
numWorkers: 50
repositories: []
imageStore: pg
dbMigrator:
image:

View File

@ -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 {

View File

@ -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)
}

View File

@ -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 {

View File

@ -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")

View File

@ -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 {

View File

@ -10,3 +10,4 @@ db:
tracker:
numWorkers: 50
repositoriesNames: []
imageStore: pg

View File

@ -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;

View File

@ -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" }}

View File

@ -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);

View File

@ -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;

View File

@ -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,

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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(

View File

@ -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,

View File

@ -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;

View File

@ -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": {

View File

@ -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": {

View File

@ -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",

View File

@ -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,

View File

@ -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;

View File

@ -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",

View File

@ -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",

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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",

View File

@ -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
internal/img/gcs/.todo Normal file
View File

58
internal/img/img.go Normal file
View File

@ -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
}

102
internal/img/pg/pg.go Normal file
View File

@ -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
internal/img/s3/.todo Normal file
View File

20
internal/util/image.go Normal file
View File

@ -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")
}
}

View File

@ -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> => {

View File

@ -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)}
/>
)}
</>
);
}

View File

@ -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}

View File

@ -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}`}>

View File

@ -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">

View File

@ -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">

View File

@ -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}`}>

View File

@ -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;
}

View File

@ -0,0 +1,8 @@
export default (): string => {
let endpoint = '';
if (process.env.NODE_ENV === 'development') {
endpoint = `${process.env.REACT_APP_API_ENDPOINT}`;
}
return endpoint;
}