From dc03aea998b795810e78af752bdf4b0b45c20449 Mon Sep 17 00:00:00 2001 From: Kairo de Araujo Date: Wed, 10 Jan 2024 13:59:29 +0100 Subject: [PATCH 1/8] refactor: rename var `mysql*` to `sql*` The current variable name `mysqlStore` and `mysqlStoreCh` can generate confusion as it comes from a factory store that support multiple databases (MySQL and Postgres). Signed-off-by: Kairo de Araujo --- cmd/archivista/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/archivista/main.go b/cmd/archivista/main.go index a2c46fea..d93e2b06 100644 --- a/cmd/archivista/main.go +++ b/cmd/archivista/main.go @@ -97,7 +97,7 @@ func main() { logrus.Fatalf("could not create ent client: %+v", err) } - mysqlStore, mysqlStoreCh, err := sqlstore.New(ctx, entClient) + sqlStore, sqlStoreCh, err := sqlstore.New(ctx, entClient) if err != nil { logrus.Fatalf("error initializing mysql client: %+v", err) } @@ -108,13 +108,13 @@ func main() { logrus.Infof("executing phase 3: create and register http service (time since start: %s)", time.Since(startTime)) // ******************************************************************************** now = time.Now() - server := server.New(mysqlStore, fileStore) + server := server.New(sqlStore, fileStore) router := mux.NewRouter() router.HandleFunc("/download/{gitoid}", server.GetHandler) router.HandleFunc("/upload", server.StoreHandler) if cfg.EnableGraphql { - client := mysqlStore.GetClient() + client := sqlStore.GetClient() srv := handler.NewDefaultServer(archivista.NewSchema(client)) srv.Use(entgql.Transactioner{TxOpener: client}) router.Handle("/query", srv) @@ -156,7 +156,7 @@ func main() { <-ctx.Done() <-fileStoreCh - <-mysqlStoreCh + <-sqlStoreCh logrus.Infof("exiting, uptime: %v", time.Since(startTime)) } From be5e8da802d5d050de5eea5f84246473424158e5 Mon Sep 17 00:00:00 2001 From: Kairo de Araujo Date: Wed, 10 Jan 2024 17:18:37 +0100 Subject: [PATCH 2/8] refactor: simplify archivista, move API to server This commit simplifies the archivista cmd (`cmd/archivista`), removing the API logic to the server, where all handlers for HTTP requests are implemented. This also includes the API Swagger documentation. Signed-off-by: Kairo Araujo --- .github/workflows/verify-licence.yml | 2 +- Makefile | 4 + cmd/archivista/main.go | 29 +--- docs/docs.go | 230 +++++++++++++++++++++++++++ docs/swagger.json | 204 ++++++++++++++++++++++++ docs/swagger.yaml | 133 ++++++++++++++++ go.mod | 11 ++ go.sum | 31 ++++ internal/server/server.go | 97 +++++++++-- 9 files changed, 708 insertions(+), 33 deletions(-) create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml diff --git a/.github/workflows/verify-licence.yml b/.github/workflows/verify-licence.yml index bf4c97c3..5ec94888 100644 --- a/.github/workflows/verify-licence.yml +++ b/.github/workflows/verify-licence.yml @@ -40,4 +40,4 @@ jobs: - name: Check license headers run: | set -e - addlicense --check -l apache -c 'The Archivista Contributors' --ignore "ent/migrate/migrations/**" -v ./ + addlicense --check -l apache -c 'The Archivista Contributors' --ignore "ent/migrate/migrations/**" --ignore "docs/**" -v ./ diff --git a/Makefile b/Makefile index 11b105ee..6103284b 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,10 @@ lint: ## Run linter @go vet ./... +.PHONY: docs +docs: ## Generate swagger docs + @go install github.com/swaggo/swag/cmd/swag@latest + @swag init -o docs -d internal/server -g server.go -pd .PHONY: db-migrations db-migrations: ## Run the migrations for the database diff --git a/cmd/archivista/main.go b/cmd/archivista/main.go index d93e2b06..7ab1c7a1 100644 --- a/cmd/archivista/main.go +++ b/cmd/archivista/main.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Archivista Contributors +// Copyright 2022-2024 The Archivista Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -30,13 +30,8 @@ import ( "syscall" "time" - "entgo.io/contrib/entgql" - "github.com/99designs/gqlgen/graphql/handler" - "github.com/99designs/gqlgen/graphql/playground" nested "github.com/antonfisher/nested-logrus-formatter" "github.com/gorilla/handlers" - "github.com/gorilla/mux" - "github.com/in-toto/archivista" "github.com/in-toto/archivista/internal/config" "github.com/in-toto/archivista/internal/metadatastorage/sqlstore" "github.com/in-toto/archivista/internal/objectstorage/blobstore" @@ -108,22 +103,10 @@ func main() { logrus.Infof("executing phase 3: create and register http service (time since start: %s)", time.Since(startTime)) // ******************************************************************************** now = time.Now() - server := server.New(sqlStore, fileStore) - router := mux.NewRouter() - router.HandleFunc("/download/{gitoid}", server.GetHandler) - router.HandleFunc("/upload", server.StoreHandler) - - if cfg.EnableGraphql { - client := sqlStore.GetClient() - srv := handler.NewDefaultServer(archivista.NewSchema(client)) - srv.Use(entgql.Transactioner{TxOpener: client}) - router.Handle("/query", srv) - if cfg.GraphqlWebClientEnable { - router.Handle("/", - playground.Handler("Archivista", "/query"), - ) - } - } + + // initialize the server + sqlClient := sqlStore.GetClient() + server := server.New(sqlStore, fileStore, cfg, sqlClient) listenAddress := cfg.ListenOn listenAddress = strings.ToLower(strings.TrimSpace(listenAddress)) @@ -146,7 +129,7 @@ func main() { handlers.AllowedOrigins(cfg.CORSAllowOrigins), handlers.AllowedMethods([]string{"GET", "POST", "OPTIONS"}), handlers.AllowedHeaders([]string{"Accept", "Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization"}), - )(router)); err != nil { + )(server.Router())); err != nil { logrus.Fatalf("unable to start http server: %+v", err) } }() diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 00000000..f33069cd --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,230 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": { + "name": "Archivista Contributors", + "url": "https://github.com/in-toto/archivista/issues/new" + }, + "license": { + "url": "https://opensource.org/licenses/Apache-2" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/donwload/{gitoid}": { + "post": { + "description": "download an attestation", + "produces": [ + "application/json" + ], + "summary": "Download", + "deprecated": true, + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dsse.Envelope" + } + } + } + } + }, + "/upload": { + "post": { + "description": "stores an attestation", + "produces": [ + "application/json" + ], + "summary": "Store", + "deprecated": true, + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.StoreResponse" + } + } + } + } + }, + "/v1/donwload/{gitoid}": { + "post": { + "description": "download an attestation", + "produces": [ + "application/json" + ], + "tags": [ + "attestation" + ], + "summary": "Download", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dsse.Envelope" + } + } + } + } + }, + "/v1/query": { + "post": { + "description": "GraphQL query", + "produces": [ + "application/json" + ], + "tags": [ + "graphql" + ], + "summary": "Query GraphQL", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/archivista.Resolver" + } + } + } + } + }, + "/v1/upload": { + "post": { + "description": "stores an attestation", + "produces": [ + "application/json" + ], + "tags": [ + "attestation" + ], + "summary": "Store", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.StoreResponse" + } + } + } + } + } + }, + "definitions": { + "api.StoreResponse": { + "type": "object", + "properties": { + "gitoid": { + "type": "string" + } + } + }, + "archivista.Resolver": { + "type": "object" + }, + "dsse.Envelope": { + "type": "object", + "properties": { + "payload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "payloadType": { + "type": "string" + }, + "signatures": { + "type": "array", + "items": { + "$ref": "#/definitions/dsse.Signature" + } + } + } + }, + "dsse.Signature": { + "type": "object", + "properties": { + "certificate": { + "type": "array", + "items": { + "type": "integer" + } + }, + "intermediates": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + "keyid": { + "type": "string" + }, + "sig": { + "type": "array", + "items": { + "type": "integer" + } + }, + "timestamps": { + "type": "array", + "items": { + "$ref": "#/definitions/dsse.SignatureTimestamp" + } + } + } + }, + "dsse.SignatureTimestamp": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer" + } + }, + "type": { + "$ref": "#/definitions/dsse.SignatureTimestampType" + } + } + }, + "dsse.SignatureTimestampType": { + "type": "string", + "enum": [ + "tsp" + ], + "x-enum-varnames": [ + "TimestampRFC3161" + ] + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "v1", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "Archivista API", + Description: "Archivista API", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 00000000..78a95e77 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,204 @@ +{ + "swagger": "2.0", + "info": { + "description": "Archivista API", + "title": "Archivista API", + "contact": { + "name": "Archivista Contributors", + "url": "https://github.com/in-toto/archivista/issues/new" + }, + "license": { + "url": "https://opensource.org/licenses/Apache-2" + }, + "version": "v1" + }, + "paths": { + "/donwload/{gitoid}": { + "post": { + "description": "download an attestation", + "produces": [ + "application/json" + ], + "summary": "Download", + "deprecated": true, + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dsse.Envelope" + } + } + } + } + }, + "/upload": { + "post": { + "description": "stores an attestation", + "produces": [ + "application/json" + ], + "summary": "Store", + "deprecated": true, + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.StoreResponse" + } + } + } + } + }, + "/v1/donwload/{gitoid}": { + "post": { + "description": "download an attestation", + "produces": [ + "application/json" + ], + "tags": [ + "attestation" + ], + "summary": "Download", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dsse.Envelope" + } + } + } + } + }, + "/v1/query": { + "post": { + "description": "GraphQL query", + "produces": [ + "application/json" + ], + "tags": [ + "graphql" + ], + "summary": "Query GraphQL", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/archivista.Resolver" + } + } + } + } + }, + "/v1/upload": { + "post": { + "description": "stores an attestation", + "produces": [ + "application/json" + ], + "tags": [ + "attestation" + ], + "summary": "Store", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.StoreResponse" + } + } + } + } + } + }, + "definitions": { + "api.StoreResponse": { + "type": "object", + "properties": { + "gitoid": { + "type": "string" + } + } + }, + "archivista.Resolver": { + "type": "object" + }, + "dsse.Envelope": { + "type": "object", + "properties": { + "payload": { + "type": "array", + "items": { + "type": "integer" + } + }, + "payloadType": { + "type": "string" + }, + "signatures": { + "type": "array", + "items": { + "$ref": "#/definitions/dsse.Signature" + } + } + } + }, + "dsse.Signature": { + "type": "object", + "properties": { + "certificate": { + "type": "array", + "items": { + "type": "integer" + } + }, + "intermediates": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + "keyid": { + "type": "string" + }, + "sig": { + "type": "array", + "items": { + "type": "integer" + } + }, + "timestamps": { + "type": "array", + "items": { + "$ref": "#/definitions/dsse.SignatureTimestamp" + } + } + } + }, + "dsse.SignatureTimestamp": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "integer" + } + }, + "type": { + "$ref": "#/definitions/dsse.SignatureTimestampType" + } + } + }, + "dsse.SignatureTimestampType": { + "type": "string", + "enum": [ + "tsp" + ], + "x-enum-varnames": [ + "TimestampRFC3161" + ] + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 00000000..8d100082 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,133 @@ +definitions: + api.StoreResponse: + properties: + gitoid: + type: string + type: object + archivista.Resolver: + type: object + dsse.Envelope: + properties: + payload: + items: + type: integer + type: array + payloadType: + type: string + signatures: + items: + $ref: '#/definitions/dsse.Signature' + type: array + type: object + dsse.Signature: + properties: + certificate: + items: + type: integer + type: array + intermediates: + items: + items: + type: integer + type: array + type: array + keyid: + type: string + sig: + items: + type: integer + type: array + timestamps: + items: + $ref: '#/definitions/dsse.SignatureTimestamp' + type: array + type: object + dsse.SignatureTimestamp: + properties: + data: + items: + type: integer + type: array + type: + $ref: '#/definitions/dsse.SignatureTimestampType' + type: object + dsse.SignatureTimestampType: + enum: + - tsp + type: string + x-enum-varnames: + - TimestampRFC3161 +info: + contact: + name: Archivista Contributors + url: https://github.com/in-toto/archivista/issues/new + description: Archivista API + license: + url: https://opensource.org/licenses/Apache-2 + title: Archivista API + version: v1 +paths: + /donwload/{gitoid}: + post: + deprecated: true + description: download an attestation + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dsse.Envelope' + summary: Download + /upload: + post: + deprecated: true + description: stores an attestation + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.StoreResponse' + summary: Store + /v1/donwload/{gitoid}: + post: + description: download an attestation + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dsse.Envelope' + summary: Download + tags: + - attestation + /v1/query: + post: + description: GraphQL query + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/archivista.Resolver' + summary: Query GraphQL + tags: + - graphql + /v1/upload: + post: + description: stores an attestation + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.StoreResponse' + summary: Store + tags: + - attestation +swagger: "2.0" diff --git a/go.mod b/go.mod index 00ce15bf..155d311a 100644 --- a/go.mod +++ b/go.mod @@ -21,12 +21,15 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.8.4 + github.com/swaggo/http-swagger/v2 v2.0.2 + github.com/swaggo/swag v1.16.2 github.com/vektah/gqlparser/v2 v2.5.11 golang.org/x/sync v0.6.0 ) require ( ariga.io/atlas v0.14.1-0.20230918065911-83ad451a4935 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect github.com/agext/levenshtein v1.2.1 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect @@ -37,6 +40,10 @@ require ( github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/inflect v0.19.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.6 // indirect + github.com/go-openapi/swag v0.19.15 // indirect github.com/go-test/deep v1.1.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -46,11 +53,13 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.3 // indirect github.com/hashicorp/hcl/v2 v2.13.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect @@ -62,6 +71,7 @@ require ( github.com/rs/xid v1.5.0 // indirect github.com/sosodev/duration v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/swaggo/files/v2 v2.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zclconf/go-cty v1.12.1 // indirect @@ -78,5 +88,6 @@ require ( golang.org/x/tools v0.13.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6820bc2f..b613800a 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ github.com/99designs/gqlgen v0.17.43 h1:I4SYg6ahjowErAQcHFVKy5EcWuwJ3+Xw9z2fLpuF github.com/99designs/gqlgen v0.17.43/go.mod h1:lO0Zjy8MkZgBdv4T1U91x09r0e0WFOdhVUutlQs1Rsc= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= @@ -56,6 +58,16 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= @@ -105,6 +117,8 @@ github.com/in-toto/go-witness v0.2.1 h1:eAxMBWUPbz3oPU3lsfEYi/Kdj6weej2umm59bOXP github.com/in-toto/go-witness v0.2.1/go.mod h1:xURJVj4QRD3xnzOJps7gT0pMCFPpAHcPqDC3EyuLuUE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= @@ -114,6 +128,7 @@ github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6K github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -125,6 +140,10 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= @@ -141,6 +160,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -172,6 +192,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= +github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= +github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg= +github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ= +github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= +github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8= github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= @@ -254,12 +280,17 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/server/server.go b/internal/server/server.go index ea213782..3df25aca 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Archivista Contributors +// Copyright 2022-2024 The Archivista Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,15 +24,24 @@ import ( "net/http" "strings" + "entgo.io/contrib/entgql" + "github.com/99designs/gqlgen/graphql/handler" + "github.com/99designs/gqlgen/graphql/playground" "github.com/edwarnicke/gitoid" "github.com/gorilla/mux" + "github.com/in-toto/archivista" + _ "github.com/in-toto/archivista/docs" + "github.com/in-toto/archivista/ent" + "github.com/in-toto/archivista/internal/config" "github.com/in-toto/archivista/pkg/api" "github.com/sirupsen/logrus" + httpSwagger "github.com/swaggo/http-swagger/v2" ) type Server struct { metadataStore Storer objectStore StorerGetter + router *mux.Router } type Storer interface { @@ -48,11 +57,51 @@ type StorerGetter interface { Getter } -func New(metadataStore Storer, objectStore StorerGetter) *Server { - return &Server{metadataStore, objectStore} +func New(metadataStore Storer, objectStore StorerGetter, cfg *config.Config, sqlClient *ent.Client) Server { + r := mux.NewRouter() + s := &Server{metadataStore, objectStore, nil} + + // TODO: remove from future version (v0.5.0) endpoint with version + r.HandleFunc("/download/{gitoid}", s.DownloadHandler) + r.HandleFunc("/upload", s.UploadHandler) + if cfg.EnableGraphql { + r.Handle("/query", s.Query(sqlClient)) + r.Handle("/v1/query", s.Query(sqlClient)) + } + + r.HandleFunc("/v1/download/{gitoid}", s.DownloadHandler) + r.HandleFunc("/v1/upload", s.UploadHandler) + if cfg.GraphqlWebClientEnable { + r.Handle("/", + playground.Handler("Archivista", "/v1/query"), + ) + } + r.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler) + s.router = r + + return *s + +} + +// @title Archivista API +// @description Archivista API +// @version v1 +// @contact.name Archivista Contributors +// @contact.url https://github.com/in-toto/archivista/issues/new +// @license Apache 2 +// @license.url https://opensource.org/licenses/Apache-2 +// InitRoutes initializes the HTTP API routes for the server +func (s *Server) Router() *mux.Router { + return s.router } -func (s *Server) Store(ctx context.Context, r io.Reader) (api.StoreResponse, error) { +// @Summary Store +// @Description stores an attestation +// @Produce json +// @Success 200 {object} api.StoreResponse +// @Tags attestation +// @Router /v1/upload [post] +func (s *Server) Upload(ctx context.Context, r io.Reader) (api.StoreResponse, error) { payload, err := io.ReadAll(r) if err != nil { return api.StoreResponse{}, err @@ -79,14 +128,20 @@ func (s *Server) Store(ctx context.Context, r io.Reader) (api.StoreResponse, err return api.StoreResponse{Gitoid: gid.String()}, nil } -func (s *Server) StoreHandler(w http.ResponseWriter, r *http.Request) { +// @Summary Store +// @Description stores an attestation +// @Produce json +// @Success 200 {object} api.StoreResponse +// @Router /upload [post] +// @Deprecated +func (s *Server) UploadHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, fmt.Sprintf("%s is an unsupported method", r.Method), http.StatusBadRequest) return } defer r.Body.Close() - resp, err := s.Store(r.Context(), r.Body) + resp, err := s.Upload(r.Context(), r.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -102,7 +157,13 @@ func (s *Server) StoreHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") } -func (s *Server) Get(ctx context.Context, gitoid string) (io.ReadCloser, error) { +// @Summary Download +// @Description download an attestation +// @Produce json +// @Success 200 {object} dsse.Envelope +// @Tags attestation +// @Router /v1/donwload/{gitoid} [post] +func (s *Server) Download(ctx context.Context, gitoid string) (io.ReadCloser, error) { if len(strings.TrimSpace(gitoid)) == 0 { return nil, errors.New("gitoid parameter is required") } @@ -119,14 +180,20 @@ func (s *Server) Get(ctx context.Context, gitoid string) (io.ReadCloser, error) return objReader, err } -func (s *Server) GetHandler(w http.ResponseWriter, r *http.Request) { +// @Summary Download +// @Description download an attestation +// @Produce json +// @Success 200 {object} dsse.Envelope +// @Deprecated +// @Router /donwload/{gitoid} [post] +func (s *Server) DownloadHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, fmt.Sprintf("%s is an unsupported method", r.Method), http.StatusBadRequest) return } vars := mux.Vars(r) - attestationReader, err := s.Get(r.Context(), vars["gitoid"]) + attestationReader, err := s.Download(r.Context(), vars["gitoid"]) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -141,3 +208,15 @@ func (s *Server) GetHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") } + +// @Summary Query GraphQL +// @Description GraphQL query +// @Produce json +// @Success 200 {object} archivista.Resolver +// @Tags graphql +// @Router /v1/query [post] +func (s *Server) Query(sqlclient *ent.Client) *handler.Server { + srv := handler.NewDefaultServer(archivista.NewSchema(sqlclient)) + srv.Use(entgql.Transactioner{TxOpener: sqlclient}) + return srv +} From 16ce17a512400d67166baedd873be15cd0268884 Mon Sep 17 00:00:00 2001 From: Kairo Araujo Date: Tue, 23 Jan 2024 14:31:05 +0100 Subject: [PATCH 3/8] refactoring: rename store APIs to upload This refactoring focuses on the code readability and maintainability easier for new contributors. `Store` -> `Upload` `StoreWithHeaders` -> `UploadWithHeaders` The `Store` will be exclusive for the Store method used by `metadataStore` and `objectStore`, causing less confusion. It renames the functions but keeps backward compatibility to allow the current users the possibility to migrate. Signed-off-by: Kairo Araujo --- pkg/api/{store.go => upload.go} | 35 ++++++++++++++++------- pkg/api/{store_test.go => upload_test.go} | 0 2 files changed, 24 insertions(+), 11 deletions(-) rename pkg/api/{store.go => upload.go} (61%) rename pkg/api/{store_test.go => upload_test.go} (100%) diff --git a/pkg/api/store.go b/pkg/api/upload.go similarity index 61% rename from pkg/api/store.go rename to pkg/api/upload.go index 937b8819..94504412 100644 --- a/pkg/api/store.go +++ b/pkg/api/upload.go @@ -26,52 +26,65 @@ import ( "github.com/in-toto/go-witness/dsse" ) -type StoreResponse struct { +type UploadResponse struct { Gitoid string `json:"gitoid"` } +// Deprecated: Use UploadResponse instead. It will be removed in version >= v0.6.0 +type StoreResponse = UploadResponse + +// Deprecated: Use Upload instead. It will be removed in version >= v0.6.0 func Store(ctx context.Context, baseUrl string, envelope dsse.Envelope) (StoreResponse, error) { + return Upload(ctx, baseUrl, envelope) +} + +func Upload(ctx context.Context, baseUrl string, envelope dsse.Envelope) (StoreResponse, error) { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) if err := enc.Encode(envelope); err != nil { return StoreResponse{}, err } - return StoreWithReader(ctx, baseUrl, buf) + return UploadWithReader(ctx, baseUrl, buf) } +// Deprecated: Use UploadWithReader instead. It will be removed in version >= v0.6.0 func StoreWithReader(ctx context.Context, baseUrl string, r io.Reader) (StoreResponse, error) { + return UploadWithReader(ctx, baseUrl, r) +} + +func UploadWithReader(ctx context.Context, baseUrl string, r io.Reader) (StoreResponse, error) { uploadPath, err := url.JoinPath(baseUrl, "upload") if err != nil { - return StoreResponse{}, err + return UploadResponse{}, err } req, err := http.NewRequestWithContext(ctx, "POST", uploadPath, r) if err != nil { - return StoreResponse{}, err + return UploadResponse{}, err } req.Header.Set("Content-Type", "application/json") hc := &http.Client{} resp, err := hc.Do(req) if err != nil { - return StoreResponse{}, err + return UploadResponse{}, err } defer resp.Body.Close() bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - return StoreResponse{}, err + return UploadResponse{}, err } if resp.StatusCode != http.StatusOK { - return StoreResponse{}, errors.New(string(bodyBytes)) + return UploadResponse{}, errors.New(string(bodyBytes)) } - storeResp := StoreResponse{} - if err := json.Unmarshal(bodyBytes, &storeResp); err != nil { - return StoreResponse{}, err + uploadResp := UploadResponse{} + if err := json.Unmarshal(bodyBytes, &uploadResp); err != nil { + return UploadResponse{}, err } - return storeResp, nil + return uploadResp, nil } diff --git a/pkg/api/store_test.go b/pkg/api/upload_test.go similarity index 100% rename from pkg/api/store_test.go rename to pkg/api/upload_test.go From 804bed92bec3112dc75daf32a6ffa7d74a60f14e Mon Sep 17 00:00:00 2001 From: Kairo Araujo Date: Tue, 23 Jan 2024 14:55:25 +0100 Subject: [PATCH 4/8] update refactoring done on API Signed-off-by: Kairo Araujo --- cmd/archivistactl/cmd/store.go | 2 +- internal/server/server.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/archivistactl/cmd/store.go b/cmd/archivistactl/cmd/store.go index ef7e76a2..87b3e59d 100644 --- a/cmd/archivistactl/cmd/store.go +++ b/cmd/archivistactl/cmd/store.go @@ -54,7 +54,7 @@ func storeAttestationByPath(ctx context.Context, baseUrl, path string) (string, } defer file.Close() - resp, err := api.StoreWithReader(ctx, baseUrl, file) + resp, err := api.UploadWithReader(ctx, baseUrl, file) if err != nil { return "", err } diff --git a/internal/server/server.go b/internal/server/server.go index 3df25aca..654ea568 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -101,31 +101,31 @@ func (s *Server) Router() *mux.Router { // @Success 200 {object} api.StoreResponse // @Tags attestation // @Router /v1/upload [post] -func (s *Server) Upload(ctx context.Context, r io.Reader) (api.StoreResponse, error) { +func (s *Server) Upload(ctx context.Context, r io.Reader) (api.UploadResponse, error) { payload, err := io.ReadAll(r) if err != nil { - return api.StoreResponse{}, err + return api.UploadResponse{}, err } gid, err := gitoid.New(bytes.NewReader(payload), gitoid.WithContentLength(int64(len(payload))), gitoid.WithSha256()) if err != nil { logrus.Errorf("failed to generate gitoid: %v", err) - return api.StoreResponse{}, err + return api.UploadResponse{}, err } if s.objectStore != nil { if err := s.objectStore.Store(ctx, gid.String(), payload); err != nil { logrus.Errorf("received error from object store: %+v", err) - return api.StoreResponse{}, err + return api.UploadResponse{}, err } } if err := s.metadataStore.Store(ctx, gid.String(), payload); err != nil { logrus.Errorf("received error from metadata store: %+v", err) - return api.StoreResponse{}, err + return api.UploadResponse{}, err } - return api.StoreResponse{Gitoid: gid.String()}, nil + return api.UploadResponse{Gitoid: gid.String()}, nil } // @Summary Store From 0a89facbd20f12c4c5c173af01f765db68db30a6 Mon Sep 17 00:00:00 2001 From: Kairo Araujo Date: Wed, 24 Jan 2024 10:28:04 +0100 Subject: [PATCH 5/8] fixup! refactoring: rename store APIs to upload Signed-off-by: Kairo Araujo --- go.mod | 1 + internal/server/server.go | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 155d311a..ecfabae4 100644 --- a/go.mod +++ b/go.mod @@ -71,6 +71,7 @@ require ( github.com/rs/xid v1.5.0 // indirect github.com/sosodev/duration v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.0 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect diff --git a/internal/server/server.go b/internal/server/server.go index 654ea568..8a987e11 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -61,7 +61,7 @@ func New(metadataStore Storer, objectStore StorerGetter, cfg *config.Config, sql r := mux.NewRouter() s := &Server{metadataStore, objectStore, nil} - // TODO: remove from future version (v0.5.0) endpoint with version + // TODO: remove from future version (v0.6.0) endpoint with version r.HandleFunc("/download/{gitoid}", s.DownloadHandler) r.HandleFunc("/upload", s.UploadHandler) if cfg.EnableGraphql { @@ -95,7 +95,7 @@ func (s *Server) Router() *mux.Router { return s.router } -// @Summary Store +// @Summary Upload // @Description stores an attestation // @Produce json // @Success 200 {object} api.StoreResponse @@ -128,7 +128,7 @@ func (s *Server) Upload(ctx context.Context, r io.Reader) (api.UploadResponse, e return api.UploadResponse{Gitoid: gid.String()}, nil } -// @Summary Store +// @Summary Upload // @Description stores an attestation // @Produce json // @Success 200 {object} api.StoreResponse @@ -162,7 +162,7 @@ func (s *Server) UploadHandler(w http.ResponseWriter, r *http.Request) { // @Produce json // @Success 200 {object} dsse.Envelope // @Tags attestation -// @Router /v1/donwload/{gitoid} [post] +// @Router /v1/download/{gitoid} [post] func (s *Server) Download(ctx context.Context, gitoid string) (io.ReadCloser, error) { if len(strings.TrimSpace(gitoid)) == 0 { return nil, errors.New("gitoid parameter is required") @@ -185,7 +185,7 @@ func (s *Server) Download(ctx context.Context, gitoid string) (io.ReadCloser, er // @Produce json // @Success 200 {object} dsse.Envelope // @Deprecated -// @Router /donwload/{gitoid} [post] +// @Router /download/{gitoid} [post] func (s *Server) DownloadHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, fmt.Sprintf("%s is an unsupported method", r.Method), http.StatusBadRequest) From aa5d56fc621dbff75c1cefee9f24ab396eff813e Mon Sep 17 00:00:00 2001 From: Kairo Araujo Date: Wed, 24 Jan 2024 12:14:41 +0100 Subject: [PATCH 6/8] tests: Add unit tests for server.go This add unit tests for main functions for server.go Signed-off-by: Kairo Araujo --- internal/server/server_test.go | 261 +++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 internal/server/server_test.go diff --git a/internal/server/server_test.go b/internal/server/server_test.go new file mode 100644 index 00000000..50b9ed4b --- /dev/null +++ b/internal/server/server_test.go @@ -0,0 +1,261 @@ +// Copyright 2024 The Archivista Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "context" + "errors" + "io" + "strings" + "testing" + + "github.com/gorilla/mux" + "github.com/in-toto/archivista/internal/config" + "github.com/in-toto/archivista/pkg/api" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +type StorerMock struct { + mock.Mock + Storer +} + +type StorerGetterMock struct { + mock.Mock + StorerGetter +} + +type UTServerSuite struct { + suite.Suite + mockedStorer *StorerMock + mockedStorerGetter *StorerGetterMock + testServer Server +} + +func TestUTServerSuite(t *testing.T) { + suite.Run(t, new(UTServerSuite)) +} + +func (ut *UTServerSuite) SetupTest() { + ut.mockedStorer = new(StorerMock) + ut.mockedStorerGetter = new(StorerGetterMock) + ut.testServer = Server{ut.mockedStorer, ut.mockedStorerGetter, nil} +} + +func (ut *UTServerSuite) Test_New() { + cfg := new(config.Config) + cfg.EnableGraphql = true + cfg.GraphqlWebClientEnable = true + ut.testServer = New(ut.mockedStorer, ut.mockedStorerGetter, cfg, nil) + ut.NotNil(ut.testServer) + router := ut.testServer.Router() + ut.NotNil(router) + + allPaths := []string{} + err := router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + pathTemplate, err := route.GetPathTemplate() + if err != nil { + ut.FailNow(err.Error()) + } + allPaths = append(allPaths, pathTemplate) + return nil + }) + if err != nil { + ut.FailNow(err.Error()) + } + ut.Contains(allPaths, "/download/{gitoid}") + ut.Contains(allPaths, "/upload") + ut.Contains(allPaths, "/query") + ut.Contains(allPaths, "/v1/download/{gitoid}") + ut.Contains(allPaths, "/v1/upload") + ut.Contains(allPaths, "/v1/query") + ut.Contains(allPaths, "/") + ut.Contains(allPaths, "/swagger/") +} + +func (ut *UTServerSuite) Test_New_EnableGraphQL_False() { + cfg := new(config.Config) + cfg.EnableGraphql = false + cfg.GraphqlWebClientEnable = true + ut.testServer = New(ut.mockedStorer, ut.mockedStorerGetter, cfg, nil) + ut.NotNil(ut.testServer) + router := ut.testServer.Router() + ut.NotNil(router) + + allPaths := []string{} + err := router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + pathTemplate, err := route.GetPathTemplate() + if err != nil { + ut.FailNow(err.Error()) + } + allPaths = append(allPaths, pathTemplate) + return nil + }) + + if err != nil { + ut.FailNow(err.Error()) + } + ut.Contains(allPaths, "/download/{gitoid}") + ut.Contains(allPaths, "/upload") + ut.NotContains(allPaths, "/query") + ut.Contains(allPaths, "/v1/download/{gitoid}") + ut.Contains(allPaths, "/v1/upload") + ut.NotContains(allPaths, "/v1/query") + ut.Contains(allPaths, "/") + ut.Contains(allPaths, "/swagger/") +} + +func (ut *UTServerSuite) Test_New_GraphqlWebClientEnable_False() { + cfg := new(config.Config) + cfg.EnableGraphql = true + cfg.GraphqlWebClientEnable = false + ut.testServer = New(ut.mockedStorer, ut.mockedStorerGetter, cfg, nil) + ut.NotNil(ut.testServer) + router := ut.testServer.Router() + ut.NotNil(router) + + allPaths := []string{} + err := router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + pathTemplate, err := route.GetPathTemplate() + if err != nil { + ut.FailNow(err.Error()) + } + allPaths = append(allPaths, pathTemplate) + return nil + }) + + if err != nil { + ut.FailNow(err.Error()) + } + ut.Contains(allPaths, "/download/{gitoid}") + ut.Contains(allPaths, "/upload") + ut.Contains(allPaths, "/query") + ut.Contains(allPaths, "/v1/download/{gitoid}") + ut.Contains(allPaths, "/v1/upload") + ut.Contains(allPaths, "/v1/query") + ut.NotContains(allPaths, "/") + ut.Contains(allPaths, "/swagger/") +} + +// Mock StorerGetter.Store() +func (m *StorerGetterMock) Store(context.Context, string, []byte) error { + args := m.Called() + return args.Error(0) +} + +// Mock StorerGetter.Get() +func (m *StorerGetterMock) Get(context.Context, string) (io.ReadCloser, error) { + args := m.Called() + stringReader := strings.NewReader("testData") + stringReadCloser := io.NopCloser(stringReader) + return stringReadCloser, args.Error(0) +} + +// Mock StorerMock.Store() +func (m *StorerMock) Store(context.Context, string, []byte) error { + args := m.Called() + return args.Error(0) +} + +func (ut *UTServerSuite) Test_Upload() { + ctx := context.TODO() + r := strings.NewReader("fakeTestData") + + ut.mockedStorerGetter.On("Store").Return(nil) // mock Get() to return nil + ut.mockedStorer.On("Store").Return(nil) // mock Store() to return nil + + resp, err := ut.testServer.Upload(ctx, r) + ut.NoError(err) + ut.NotEqual("", resp.Gitoid) +} + +func (ut *UTServerSuite) Test_Upload_NoObjectStorage() { + ctx := context.TODO() + r := strings.NewReader("fakeTestData") + + ut.testServer.objectStore = nil + ut.mockedStorer.On("Store").Return(nil) // mock Store() to return nil + + resp, err := ut.testServer.Upload(ctx, r) + ut.NoError(err) + ut.NotEqual("", resp.Gitoid) +} + +func (ut *UTServerSuite) Test_Upload_FailedObjectStorage() { + ctx := context.TODO() + r := strings.NewReader("fakeTestData") + + ut.mockedStorerGetter.On("Store").Return(errors.New("Bad S3")) // mock Get() to return err + ut.mockedStorer.On("Store").Return(nil) // mock Store() to return nil + + resp, err := ut.testServer.Upload(ctx, r) + ut.ErrorContains(err, "Bad S3") + ut.Equal(api.UploadResponse{}, resp) +} + +func (ut *UTServerSuite) Test_Upload_FailedMetadatStprage() { + ctx := context.TODO() + r := strings.NewReader("fakeTestData") + + ut.mockedStorerGetter.On("Store").Return(nil) // mock Get() to return nil + ut.mockedStorer.On("Store").Return(errors.New("Bad SQL")) // mock Store() to return err + + resp, err := ut.testServer.Upload(ctx, r) + ut.ErrorContains(err, "Bad SQL") + ut.Equal(api.UploadResponse{}, resp) +} + +func (ut *UTServerSuite) Test_Download() { + ctx := context.TODO() + ut.mockedStorerGetter.On("Get").Return(nil) // mock Get() to return nil + + resp, err := ut.testServer.Download(ctx, "fakeGitoid") + ut.NoError(err) + data, _ := io.ReadAll(resp) + ut.Equal("testData", string(data)) +} + +func (ut *UTServerSuite) Test_Download_EmptyGitoid() { + ctx := context.TODO() + ut.mockedStorerGetter.On("Get").Return(nil) // mock Get() to return nil + + _, err := ut.testServer.Download(ctx, "") + ut.ErrorContains(err, "gitoid parameter is required") +} + +func (ut *UTServerSuite) Test_Download_EmptyGitoidTrimmed() { + ctx := context.TODO() + ut.mockedStorerGetter.On("Get").Return(nil) // mock Get() to return nil + + _, err := ut.testServer.Download(ctx, " ") + ut.ErrorContains(err, "gitoid parameter is required") +} + +func (ut *UTServerSuite) Test_Download_NoObjectStorage() { + ctx := context.TODO() + ut.testServer.objectStore = nil + + _, err := ut.testServer.Download(ctx, "fakeGitoid") + ut.ErrorContains(err, "object store unavailable") +} + +func (ut *UTServerSuite) Test_Download_ObjectStorageError() { + ctx := context.TODO() + ut.mockedStorerGetter.On("Get").Return(errors.New("BAD S3")) // mock Get() to return nil + + _, err := ut.testServer.Download(ctx, "fakeGitoid") + ut.ErrorContains(err, "BAD S3") +} From 8e3f11451634143a417992ea8982b47a2258a909 Mon Sep 17 00:00:00 2001 From: Kairo Araujo Date: Wed, 24 Jan 2024 15:34:48 +0100 Subject: [PATCH 7/8] fix: Add more consistent HTTP errors This commit fixes some status codes that can cause misunderstanding. Some errors were returning Bad Request (400) even when the user sends a correct request. The errors originating from the infrastructure or error during operations should raise Internal Server Error (500) as they are not expected. Unit tests are included to avoid regression Signed-off-by: Kairo Araujo --- internal/server/server.go | 11 ++-- internal/server/server_test.go | 91 ++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index 8a987e11..c207eff4 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -143,7 +143,7 @@ func (s *Server) UploadHandler(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() resp, err := s.Upload(r.Context(), r.Body) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -193,16 +193,21 @@ func (s *Server) DownloadHandler(w http.ResponseWriter, r *http.Request) { } vars := mux.Vars(r) + if vars == nil { + http.Error(w, "gitoid parameter is required", http.StatusBadRequest) + return + } + attestationReader, err := s.Download(r.Context(), vars["gitoid"]) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, err.Error(), http.StatusInternalServerError) return } defer attestationReader.Close() if _, err := io.Copy(w, attestationReader); err != nil { logrus.Errorf("failed to copy attestation to response: %+v", err) - w.WriteHeader(http.StatusBadRequest) + w.WriteHeader(http.StatusInternalServerError) return } diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 50b9ed4b..d573b197 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -15,9 +15,12 @@ package server import ( + "bytes" "context" "errors" "io" + "net/http" + "net/http/httptest" "strings" "testing" @@ -218,6 +221,47 @@ func (ut *UTServerSuite) Test_Upload_FailedMetadatStprage() { ut.Equal(api.UploadResponse{}, resp) } +func (ut *UTServerSuite) Test_UploadHandler() { + + w := httptest.NewRecorder() + requestBody := []byte("fakePayload") + request := httptest.NewRequest(http.MethodPost, "/v1/upload", bytes.NewBuffer(requestBody)) + + ut.mockedStorerGetter.On("Store").Return(nil) // mock Get() to return nil + ut.mockedStorer.On("Store").Return(nil) // mock Store() to return nil + + ut.testServer.UploadHandler(w, request) + ut.Equal(http.StatusOK, w.Code) +} + +func (ut *UTServerSuite) Test_UploadHandler_WrongMethod() { + + w := httptest.NewRecorder() + requestBody := []byte("fakePayload") + request := httptest.NewRequest(http.MethodGet, "/upload", bytes.NewBuffer(requestBody)) + + ut.mockedStorerGetter.On("Store").Return(nil) // mock Get() to return nil + ut.mockedStorer.On("Store").Return(nil) // mock Store() to return nil + + ut.testServer.UploadHandler(w, request) + ut.Equal(http.StatusBadRequest, w.Code) + ut.Contains(w.Body.String(), "is an unsupported method") +} + +func (ut *UTServerSuite) Test_UploadHandler_FailureUpload() { + + w := httptest.NewRecorder() + requestBody := []byte("fakePayload") + request := httptest.NewRequest(http.MethodPost, "/upload", bytes.NewBuffer(requestBody)) + + ut.mockedStorerGetter.On("Store").Return(errors.New("BAD S3")) // mock Get() to return nil + ut.mockedStorer.On("Store").Return(nil) // mock Store() to return nil + + ut.testServer.UploadHandler(w, request) + ut.Equal(http.StatusInternalServerError, w.Code) + ut.Contains(w.Body.String(), "BAD S3") +} + func (ut *UTServerSuite) Test_Download() { ctx := context.TODO() ut.mockedStorerGetter.On("Get").Return(nil) // mock Get() to return nil @@ -259,3 +303,50 @@ func (ut *UTServerSuite) Test_Download_ObjectStorageError() { _, err := ut.testServer.Download(ctx, "fakeGitoid") ut.ErrorContains(err, "BAD S3") } + +func (ut *UTServerSuite) Test_DownloadHandler() { + w := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/v1/download", nil) + request = mux.SetURLVars(request, map[string]string{"gitoid": "fakeGitoid"}) + + ut.mockedStorerGetter.On("Get").Return(nil) // mock Get() to return nil + + ut.testServer.DownloadHandler(w, request) + ut.Equal(http.StatusOK, w.Code) + ut.Equal("testData", w.Body.String()) +} + +func (ut *UTServerSuite) Test_DownloadHandler_BadMethod() { + w := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodPost, "/v1/download", nil) + request = mux.SetURLVars(request, map[string]string{"gitoid": "fakeGitoid"}) + + ut.mockedStorerGetter.On("Get").Return(nil) // mock Get() to return nil + + ut.testServer.DownloadHandler(w, request) + ut.Equal(http.StatusBadRequest, w.Code) + ut.Contains(w.Body.String(), "POST is an unsupported method") +} + +func (ut *UTServerSuite) Test_DownloadHandler_MissingGitOID() { + w := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/v1/download", nil) + + ut.mockedStorerGetter.On("Get").Return(nil) // mock Get() to return nil + + ut.testServer.DownloadHandler(w, request) + ut.Equal(http.StatusBadRequest, w.Code) + ut.Contains(w.Body.String(), "gitoid parameter is required") +} + +func (ut *UTServerSuite) Test_DownloadHandler_ObjectStorageFailed() { + w := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/v1/download", nil) + request = mux.SetURLVars(request, map[string]string{"gitoid": "fakeGitoid"}) + + ut.mockedStorerGetter.On("Get").Return(errors.New("BAD S3")) // mock Get() to return nil + + ut.testServer.DownloadHandler(w, request) + ut.Equal(http.StatusInternalServerError, w.Code) + ut.Contains(w.Body.String(), "BAD S3") +} From 1aef60c769792fe57a79bde37ff9ebc5af063611 Mon Sep 17 00:00:00 2001 From: Kairo Araujo Date: Wed, 24 Jan 2024 20:19:56 +0100 Subject: [PATCH 8/8] fixup! refactor: simplify archivista, move API to server Signed-off-by: Kairo Araujo --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6103284b..5add6a1b 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,7 @@ lint: ## Run linter .PHONY: docs docs: ## Generate swagger docs - @go install github.com/swaggo/swag/cmd/swag@latest + @go install github.com/swaggo/swag/cmd/swag@v1.16.2 @swag init -o docs -d internal/server -g server.go -pd .PHONY: db-migrations