Skip to content

Commit

Permalink
Merge pull request #59 from anchore/feat/use-v2-api
Browse files Browse the repository at this point in the history
feat: use v2 API if available
  • Loading branch information
robcresswell authored Aug 21, 2023
2 parents 6f0c7f6 + f65499b commit a693582
Show file tree
Hide file tree
Showing 4 changed files with 323 additions and 35 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.20
require (
github.com/adrg/xdg v0.4.0
github.com/aws/aws-sdk-go v1.44.163
github.com/h2non/gock v1.2.0
github.com/mitchellh/go-homedir v1.1.0
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.14.0
Expand All @@ -16,6 +17,7 @@ require (
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
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/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
Expand Down Expand Up @@ -149,6 +153,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
Expand Down
148 changes: 120 additions & 28 deletions pkg/reporter/reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,72 +6,164 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"

"github.com/h2non/gock"

"github.com/anchore/ecs-inventory/internal/logger"
"github.com/anchore/ecs-inventory/internal/tracker"
"github.com/anchore/ecs-inventory/pkg/connection"
)

const ReportAPIPath = "v1/enterprise/ecs-inventory"
const v1ReportAPIPath = "v1/enterprise/ecs-inventory"
const v2ReportAPIPath = "v2/ecs-inventory"

var apiPath = v2ReportAPIPath

// This method does the actual Reporting (via HTTP) to Anchore
//
//nolint:gosec
func Post(report Report, anchoreDetails connection.AnchoreInfo) error {
logger.Log.Info("Reporting results to Anchore")
defer tracker.TrackFunctionTime(time.Now(), fmt.Sprintf("Posting Inventory Report for cluster %s", report.ClusterARN))
logger.Log.Info("Reporting results to Anchore", "Account", anchoreDetails.Account)
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: anchoreDetails.HTTP.Insecure},
}
} // #nosec G402
client := &http.Client{
Transport: tr,
Timeout: time.Duration(anchoreDetails.HTTP.TimeoutSeconds) * time.Second,
}
gock.InterceptClient(client)

req, err := prepareRequest(report, anchoreDetails)

if err != nil {
return err
}

resp, err := client.Do(req)
if err != nil {
if resp != nil && resp.StatusCode == 401 {
return fmt.Errorf("failed to report data to Anchore, check credentials: %w", err)
}
return fmt.Errorf("failed to report data to Anchore: %w", err)
}
defer resp.Body.Close()

// If we get a 404, make an assumption that the backend API support may have
// changed, either because our default v2 is too new or because the API
// service has been upgraded. Check the version, and if the version changes,
// cache it and retry the request
if resp.StatusCode == 404 {
previousAPIPath := apiPath
apiPath, err = fetchVersionedAPIPath(anchoreDetails)
if err != nil {
return fmt.Errorf("failed to validate Enterprise API: %w", err)
}
apiEndpoint, err := url.JoinPath(anchoreDetails.URL, apiPath)
if err != nil {
return fmt.Errorf("failed to parse API URL: %w", err)
}

if apiPath != previousAPIPath {
logger.Log.Info("Retrying inventory report with new endpoint", "apiEndpoint", apiEndpoint)
return Post(report, anchoreDetails)
}

return fmt.Errorf("failed to report data to Anchore: %+v", resp)
}

if resp.StatusCode < 200 || resp.StatusCode > 299 {
return fmt.Errorf("failed to report data to Anchore: %+v", resp)
}

respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response from Anchore: %w", err)
}
if len(respBody) > 0 && !json.Valid(respBody) {
logger.Log.Debug("Anchore response body: ", string(respBody))
return fmt.Errorf("failed to report data to Anchore not a valid json response: %+v", resp)
}
logger.Log.Debug("Successfully reported results to Anchore")
return nil
}

anchoreURL, err := buildURL(anchoreDetails)
func prepareRequest(report Report, anchoreDetails connection.AnchoreInfo) (*http.Request, error) {
apiEndpoint, err := url.JoinPath(anchoreDetails.URL, apiPath)
if err != nil {
return fmt.Errorf("failed to build url: %w", err)
return nil, fmt.Errorf("failed to parse API URL: %w", err)
}
logger.Log.Debug("Reporting results to Anchore", "Endpoint", apiEndpoint)

reqBody, err := json.Marshal(report)
if err != nil {
return fmt.Errorf("failed to serialize results as JSON: %w", err)
return nil, fmt.Errorf("failed to serialize results as JSON: %w", err)
}

req, err := http.NewRequest("POST", anchoreURL, bytes.NewBuffer(reqBody))
req, err := http.NewRequest("POST", apiEndpoint, bytes.NewBuffer(reqBody))
if err != nil {
return fmt.Errorf("failed to build request to report data to Anchore: %w", err)
return nil, fmt.Errorf("failed to build request to report data to Anchore: %w", err)
}
req.SetBasicAuth(anchoreDetails.User, anchoreDetails.Password)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-anchore-account", anchoreDetails.Account)
resp, err := client.Do(req)

return req, nil
}

type AnchoreVersion struct {
API struct {
Version string `json:"version"`
} `json:"api"`
DB struct {
SchemaVersion string `json:"schema_version"`
} `json:"db"`
Service struct {
Version string `json:"version"`
} `json:"service"`
}

func fetchVersionedAPIPath(anchoreDetails connection.AnchoreInfo) (string, error) {
logger.Log.Debug("Detecting Anchore API version")
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: anchoreDetails.HTTP.Insecure},
} // #nosec G402
client := &http.Client{
Transport: tr,
Timeout: time.Duration(anchoreDetails.HTTP.TimeoutSeconds) * time.Second,
}
gock.InterceptClient(client) // Required to use gock for testing custom client

versionEndpoint, err := url.JoinPath(anchoreDetails.URL, "version")
if err != nil {
if resp != nil {
if resp.StatusCode == 401 {
return fmt.Errorf("failed to report data to Anchore, check credentials: %w", err)
}
}
return fmt.Errorf("failed to report data to Anchore: %w", err)
return v1ReportAPIPath, fmt.Errorf("failed to parse API URL: %w", err)
}

resp, err := client.Get(versionEndpoint)
if err != nil {
return v1ReportAPIPath, fmt.Errorf("failed to contact Anchore API: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return fmt.Errorf("failed to report data to Anchore: %+v", resp)
if resp.StatusCode != 200 {
return v1ReportAPIPath, fmt.Errorf("failed to retrieve Anchore API version: %+v", resp)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return v1ReportAPIPath, fmt.Errorf("failed to read Anchore API version: %w", err)
}
logger.Log.Debug("Successfully reported results to Anchore", "Account", anchoreDetails.Account)
return nil
}

func buildURL(anchoreDetails connection.AnchoreInfo) (string, error) {
anchoreURL, err := url.Parse(anchoreDetails.URL)
ver := AnchoreVersion{}
err = json.Unmarshal(body, &ver)
if err != nil {
return "", err
return v1ReportAPIPath, fmt.Errorf("failed to parse API version: %w", err)
}

anchoreURL.Path += ReportAPIPath
logger.Log.Debugf("Anchore API version: %v", ver)

if ver.API.Version == "2" {
return v2ReportAPIPath, nil
}

return anchoreURL.String(), nil
return v1ReportAPIPath, nil
}
Loading

0 comments on commit a693582

Please sign in to comment.