Skip to content

Commit

Permalink
feat: use v2 API if available
Browse files Browse the repository at this point in the history
This patch adds support for using the new v2 API by default, and
falling back to v1. This shouldn't be a breaking change until we remove
support for the v1 API

Signed-off-by: Rob Cresswell <robcresswell@users.noreply.github.com>
  • Loading branch information
robcresswell committed Aug 21, 2023
1 parent 44023bc commit f65499b
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
}

0 comments on commit f65499b

Please sign in to comment.