Incorporate import changes + add image overwrite option (#294)

* incorporate import changes + add image overwrite option

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update import tests to account for arbitrary json shape

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman
2020-12-18 16:59:30 -05:00
committed by GitHub
parent 75d89293ce
commit 6aaf9ee712
13 changed files with 120 additions and 56 deletions
+23 -8
View File
@@ -6,6 +6,8 @@ import (
"fmt"
"time"
"github.com/antihax/optional"
"github.com/anchore/client-go/pkg/external"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/internal/bus"
@@ -17,6 +19,15 @@ import (
"github.com/wagoodman/go-progress"
)
type ImportConfig struct {
ImageMetadata image.Metadata
SourceMetadata source.Metadata
Catalog *pkg.Catalog
Distro *distro.Distro
Dockerfile []byte
OverwriteExistingUpload bool
}
func importProgress(source string) (*progress.Stage, *progress.Manual) {
stage := &progress.Stage{}
prog := &progress.Manual{
@@ -39,8 +50,8 @@ func importProgress(source string) (*progress.Stage, *progress.Manual) {
}
// nolint:funlen
func (c *Client) Import(ctx context.Context, imageMetadata image.Metadata, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, dockerfile []byte) error {
stage, prog := importProgress(imageMetadata.ID)
func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
stage, prog := importProgress(c.config.Hostname)
ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Second*30)
defer cancel()
@@ -60,33 +71,37 @@ func (c *Client) Import(ctx context.Context, imageMetadata image.Metadata, s sou
prog.N++
sessionID := startOperation.Uuid
packageDigest, err := importPackageSBOM(authedCtx, c.client.ImportsApi, sessionID, s, catalog, d, stage)
packageDigest, err := importPackageSBOM(authedCtx, c.client.ImportsApi, sessionID, cfg.SourceMetadata, cfg.Catalog, cfg.Distro, stage)
if err != nil {
return fmt.Errorf("failed to import Package SBOM: %w", err)
}
prog.N++
manifestDigest, err := importManifest(authedCtx, c.client.ImportsApi, sessionID, imageMetadata.RawManifest, stage)
manifestDigest, err := importManifest(authedCtx, c.client.ImportsApi, sessionID, cfg.ImageMetadata.RawManifest, stage)
if err != nil {
return fmt.Errorf("failed to import Manifest: %w", err)
}
prog.N++
configDigest, err := importConfig(authedCtx, c.client.ImportsApi, sessionID, imageMetadata.RawConfig, stage)
configDigest, err := importConfig(authedCtx, c.client.ImportsApi, sessionID, cfg.ImageMetadata.RawConfig, stage)
if err != nil {
return fmt.Errorf("failed to import Config: %w", err)
}
prog.N++
dockerfileDigest, err := importDockerfile(authedCtx, c.client.ImportsApi, sessionID, dockerfile, stage)
dockerfileDigest, err := importDockerfile(authedCtx, c.client.ImportsApi, sessionID, cfg.Dockerfile, stage)
if err != nil {
return fmt.Errorf("failed to import Dockerfile: %w", err)
}
prog.N++
stage.Current = "finalizing"
imageModel := addImageModel(imageMetadata, packageDigest, manifestDigest, dockerfileDigest, configDigest, sessionID)
_, _, err = c.client.ImagesApi.AddImage(authedCtx, imageModel, nil)
imageModel := addImageModel(cfg.ImageMetadata, packageDigest, manifestDigest, dockerfileDigest, configDigest, sessionID)
opts := external.AddImageOpts{
Force: optional.NewBool(cfg.OverwriteExistingUpload),
}
_, _, err = c.client.ImagesApi.AddImage(authedCtx, imageModel, &opts)
if err != nil {
var detail = "no details given"
var openAPIErr external.GenericOpenAPIError
+11 -4
View File
@@ -3,6 +3,7 @@ package anchore
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
@@ -14,15 +15,21 @@ import (
)
type configImportAPI interface {
ImportImageConfig(ctx context.Context, sessionID string, contents string) (external.ImageImportContentResponse, *http.Response, error)
ImportImageConfig(ctx context.Context, sessionID string, contents interface{}) (external.ImageImportContentResponse, *http.Response, error)
}
func importConfig(ctx context.Context, api configImportAPI, sessionID string, manifest []byte, stage *progress.Stage) (string, error) {
if len(manifest) > 0 {
func importConfig(ctx context.Context, api configImportAPI, sessionID string, config []byte, stage *progress.Stage) (string, error) {
if len(config) > 0 {
log.Debug("importing image config")
stage.Current = "image config"
response, httpResponse, err := api.ImportImageConfig(ctx, sessionID, string(manifest))
// API requires an object, but we do not verify the shape of this object locally
var sender map[string]interface{}
if err := json.Unmarshal(config, &sender); err != nil {
return "", err
}
response, httpResponse, err := api.ImportImageConfig(ctx, sessionID, sender)
if err != nil {
var openAPIErr external.GenericOpenAPIError
if errors.As(err, &openAPIErr) {
+25 -19
View File
@@ -2,6 +2,7 @@ package anchore
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
@@ -16,7 +17,7 @@ import (
type mockConfigImportAPI struct {
sessionID string
model string
model interface{}
httpResponse *http.Response
err error
ctx context.Context
@@ -24,7 +25,7 @@ type mockConfigImportAPI struct {
wasCalled bool
}
func (m *mockConfigImportAPI) ImportImageConfig(ctx context.Context, sessionID string, contents string) (external.ImageImportContentResponse, *http.Response, error) {
func (m *mockConfigImportAPI) ImportImageConfig(ctx context.Context, sessionID string, contents interface{}) (external.ImageImportContentResponse, *http.Response, error) {
m.wasCalled = true
m.model = contents
m.sessionID = sessionID
@@ -41,16 +42,16 @@ func TestConfigImport(t *testing.T) {
sessionID := "my-session"
tests := []struct {
name string
manifest string
api *mockConfigImportAPI
expectsError bool
expectsCall bool
name string
manifestJSONStr string
api *mockConfigImportAPI
expectsError bool
expectsCall bool
}{
{
name: "Go case: import works",
manifest: "the-manifest-contents!",
name: "Go case: import works",
manifestJSONStr: `{ "key": "the-manifest-contents!" }`,
api: &mockConfigImportAPI{
httpResponse: &http.Response{StatusCode: 200},
responseDigest: "digest!",
@@ -58,14 +59,14 @@ func TestConfigImport(t *testing.T) {
expectsCall: true,
},
{
name: "No manifest provided",
manifest: "",
api: &mockConfigImportAPI{},
expectsCall: false,
name: "No manifest provided",
manifestJSONStr: "",
api: &mockConfigImportAPI{},
expectsCall: false,
},
{
name: "API returns an error",
manifest: "the-manifest-contents!",
name: "API returns an error",
manifestJSONStr: `{ "key": "the-manifest-contents!" }`,
api: &mockConfigImportAPI{
err: fmt.Errorf("api error, something went wrong"),
},
@@ -73,8 +74,8 @@ func TestConfigImport(t *testing.T) {
expectsCall: true,
},
{
name: "API HTTP-level error",
manifest: "the-manifest-contents!",
name: "API HTTP-level error",
manifestJSONStr: `{ "key": "the-manifest-contents!" }`,
api: &mockConfigImportAPI{
httpResponse: &http.Response{StatusCode: 404},
},
@@ -86,7 +87,7 @@ func TestConfigImport(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
digest, err := importConfig(context.TODO(), test.api, sessionID, []byte(test.manifest), &progress.Stage{})
digest, err := importConfig(context.TODO(), test.api, sessionID, []byte(test.manifestJSONStr), &progress.Stage{})
// validate error handling
if err != nil && !test.expectsError {
@@ -114,7 +115,12 @@ func TestConfigImport(t *testing.T) {
t.Errorf("different session ID: %s != %s", test.api.sessionID, sessionID)
}
for _, d := range deep.Equal(test.api.model, test.manifest) {
var expected map[string]interface{}
if err := json.Unmarshal([]byte(test.manifestJSONStr), &expected); err != nil {
t.Fatalf("could not unmarshal expected results")
}
for _, d := range deep.Equal(test.api.model, expected) {
t.Errorf("model difference: %s", d)
}
-1
View File
@@ -1,4 +1,3 @@
// nolint:dupl
package anchore
import (
+9 -2
View File
@@ -3,6 +3,7 @@ package anchore
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
@@ -14,7 +15,7 @@ import (
)
type manifestImportAPI interface {
ImportImageManifest(ctx context.Context, sessionID string, contents string) (external.ImageImportContentResponse, *http.Response, error)
ImportImageManifest(ctx context.Context, sessionID string, contents interface{}) (external.ImageImportContentResponse, *http.Response, error)
}
func importManifest(ctx context.Context, api manifestImportAPI, sessionID string, manifest []byte, stage *progress.Stage) (string, error) {
@@ -22,7 +23,13 @@ func importManifest(ctx context.Context, api manifestImportAPI, sessionID string
log.Debug("importing image manifest")
stage.Current = "image manifest"
response, httpResponse, err := api.ImportImageManifest(ctx, sessionID, string(manifest))
// API requires an object, but we do not verify the shape of this object locally
var sender map[string]interface{}
if err := json.Unmarshal(manifest, &sender); err != nil {
return "", err
}
response, httpResponse, err := api.ImportImageManifest(ctx, sessionID, sender)
if err != nil {
var openAPIErr external.GenericOpenAPIError
if errors.As(err, &openAPIErr) {
+12 -6
View File
@@ -2,6 +2,7 @@ package anchore
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
@@ -16,7 +17,7 @@ import (
type mockManifestImportAPI struct {
sessionID string
model string
model interface{}
httpResponse *http.Response
err error
ctx context.Context
@@ -24,7 +25,7 @@ type mockManifestImportAPI struct {
wasCalled bool
}
func (m *mockManifestImportAPI) ImportImageManifest(ctx context.Context, sessionID string, contents string) (external.ImageImportContentResponse, *http.Response, error) {
func (m *mockManifestImportAPI) ImportImageManifest(ctx context.Context, sessionID string, contents interface{}) (external.ImageImportContentResponse, *http.Response, error) {
m.wasCalled = true
m.model = contents
m.sessionID = sessionID
@@ -50,7 +51,7 @@ func TestManifestImport(t *testing.T) {
{
name: "Go case: import works",
manifest: "the-manifest-contents!",
manifest: `{ "key": "the-config-contents!" }`,
api: &mockManifestImportAPI{
httpResponse: &http.Response{StatusCode: 200},
responseDigest: "digest!",
@@ -65,7 +66,7 @@ func TestManifestImport(t *testing.T) {
},
{
name: "API returns an error",
manifest: "the-manifest-contents!",
manifest: `{ "key": "the-config-contents!" }`,
api: &mockManifestImportAPI{
err: fmt.Errorf("api error, something went wrong"),
},
@@ -74,7 +75,7 @@ func TestManifestImport(t *testing.T) {
},
{
name: "API HTTP-level error",
manifest: "the-manifest-contents!",
manifest: `{ "key": "the-config-contents!" }`,
api: &mockManifestImportAPI{
httpResponse: &http.Response{StatusCode: 404},
},
@@ -114,7 +115,12 @@ func TestManifestImport(t *testing.T) {
t.Errorf("different session ID: %s != %s", test.api.sessionID, sessionID)
}
for _, d := range deep.Equal(test.api.model, test.manifest) {
var expected map[string]interface{}
if err := json.Unmarshal([]byte(test.manifest), &expected); err != nil {
t.Fatalf("could not unmarshal expected results")
}
for _, d := range deep.Equal(test.api.model, expected) {
t.Errorf("model difference: %s", d)
}
+7 -6
View File
@@ -46,12 +46,13 @@ type logging struct {
type anchore struct {
// upload options
UploadEnabled bool `yaml:"upload-enabled" mapstructure:"upload-enabled"` // whether to upload results to Anchore Engine/Enterprise (defaults to "false" unless there is the presence of -h CLI option)
Host string `yaml:"host" mapstructure:"host"` // -H , hostname of the engine/enterprise instance to upload to
Path string `yaml:"path" mapstructure:"path"` // override the engine/enterprise API upload path
Username string `yaml:"username" mapstructure:"username"` // -u , username to authenticate upload
Password string `yaml:"password" mapstructure:"password"` // -p , password to authenticate upload
Dockerfile string `yaml:"dockerfile" mapstructure:"dockerfile"` // -d , dockerfile to attach for upload
UploadEnabled bool `yaml:"upload-enabled" mapstructure:"upload-enabled"` // whether to upload results to Anchore Engine/Enterprise (defaults to "false" unless there is the presence of -h CLI option)
Host string `yaml:"host" mapstructure:"host"` // -H , hostname of the engine/enterprise instance to upload to
Path string `yaml:"path" mapstructure:"path"` // override the engine/enterprise API upload path
Username string `yaml:"username" mapstructure:"username"` // -u , username to authenticate upload
Password string `yaml:"password" mapstructure:"password"` // -p , password to authenticate upload
Dockerfile string `yaml:"dockerfile" mapstructure:"dockerfile"` // -d , dockerfile to attach for upload
OverwriteExistingImage bool `yaml:"overwrite-existing-image" mapstructure:"overwrite-existing-image"` // --overwrite-existing-image , if any of the SBOM components have already been uploaded this flag will ensure they are overwritten with the current upload
}
type Development struct {