Skip to content

Commit

Permalink
Merge pull request #98 from anchore/svietry/v2-api
Browse files Browse the repository at this point in the history
fix: address the v2 api endpoint
  • Loading branch information
bradleyjones authored Aug 15, 2023
2 parents 5c8a6ac + 0f987d1 commit 8ae17e5
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 37 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ require (
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/h2non/gock v1.2.0 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,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 @@ -205,6 +209,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
Expand Down
95 changes: 60 additions & 35 deletions pkg/reporter/reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,32 @@ import (
"github.com/anchore/k8s-inventory/internal/log"
"github.com/anchore/k8s-inventory/internal/tracker"
"github.com/anchore/k8s-inventory/pkg/inventory"
"github.com/h2non/gock"
)

const reportAPIPathV1 = "v1/enterprise/kubernetes-inventory"
const reportAPIPathV2 = "v2/kubernetes-inventory"
const (
reportAPIPathV1 = "v1/enterprise/kubernetes-inventory"
reportAPIPathV2 = "v2/kubernetes-inventory"
)

var cachedVersion = 0
var enterpriseEndpoint = reportAPIPathV2

// This method does the actual Reporting (via HTTP) to Anchore
//
//nolint:gosec
func Post(report inventory.Report, anchoreDetails config.AnchoreInfo) error {
defer tracker.TrackFunctionTime(time.Now(), "Reporting results to Anchore for cluster: "+report.ClusterName+"")
log.Debug("Reporting results to Anchore")
log.Debug("Reporting results to Anchore using endpoint: ", enterpriseEndpoint)
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: anchoreDetails.HTTP.Insecure},
}
client := &http.Client{
Transport: tr,
Timeout: time.Duration(anchoreDetails.HTTP.TimeoutSeconds) * time.Second,
}
gock.InterceptClient(client) // Required to use gock for testing custom client

version, err := getVersion(anchoreDetails)
if err != nil {
return err
}

anchoreURL, err := buildURL(anchoreDetails, version)
anchoreURL, err := buildURL(anchoreDetails, enterpriseEndpoint)
if err != nil {
return fmt.Errorf("failed to build url: %w", err)
}
Expand All @@ -53,7 +52,7 @@ func Post(report inventory.Report, anchoreDetails config.AnchoreInfo) error {

req, err := http.NewRequest("POST", anchoreURL, bytes.NewBuffer(reqBody))
if err != nil {
return fmt.Errorf("failed to build request to report data to Anchore: %w", err)
return fmt.Errorf("failed to send data to Anchore: %w", err)
}
req.SetBasicAuth(anchoreDetails.User, anchoreDetails.Password)
req.Header.Set("Content-Type", "application/json")
Expand All @@ -63,22 +62,43 @@ func Post(report inventory.Report, anchoreDetails config.AnchoreInfo) error {
return fmt.Errorf("failed to report data to Anchore: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == 404 {
previousVersion := enterpriseEndpoint
// We failed to send the inventory. We need to check the version of Enterprise.
versionError := checkVersion(anchoreDetails)
if versionError != nil {
return fmt.Errorf("failed to validate Enterprise API: %w", versionError)
}
if previousVersion != enterpriseEndpoint {
// We need to re-send the inventory with the new endpoint
log.Info("Retrying inventory report with new endpoint: %s", enterpriseEndpoint)
return Post(report, anchoreDetails)
}
}
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return fmt.Errorf("failed to report data to Anchore: %+v", resp)
}
log.Debug("Successfully reported results to Anchore")
return 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"`
}

// This method retrieves the API version from Anchore
// and caches the response if parsed successfully
//
//nolint:gosec
func getVersion(anchoreDetails config.AnchoreInfo) (int, error) {
if cachedVersion > 0 {
return cachedVersion, nil
}

func checkVersion(anchoreDetails config.AnchoreInfo) error {
log.Debug("Detecting Anchore API version")
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: anchoreDetails.HTTP.Insecure},
Expand All @@ -87,42 +107,47 @@ func getVersion(anchoreDetails config.AnchoreInfo) (int, error) {
Transport: tr,
Timeout: time.Duration(anchoreDetails.HTTP.TimeoutSeconds) * time.Second,
}
resp, err := client.Get(anchoreDetails.URL)
gock.InterceptClient(client) // Required to use gock for testing custom client

resp, err := client.Get(anchoreDetails.URL + "/version")
if err != nil {
return 0, fmt.Errorf("failed to contact Anchore API: %w", err)
return fmt.Errorf("failed to contact Anchore API: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return 0, fmt.Errorf("failed to retrieve Anchore API version: %+v", resp)
fmt.Println("fff")
return fmt.Errorf("failed to retrieve Anchore API version: %+v", resp)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, fmt.Errorf("failed to read Anchore API version: %w", err)
return fmt.Errorf("failed to read Anchore API version: %w", err)
}

switch version := string(body); version {
case "v1":
cachedVersion = 1
return 1, nil
case "v2":
cachedVersion = 2
return 2, nil
default:
return 0, fmt.Errorf("unexpected Anchore API version: %s", version)
ver := AnchoreVersion{}
err = json.Unmarshal(body, &ver)
if err != nil {
return fmt.Errorf("failed to parse API version: %w", err)
}

log.Debug("Anchore API version: ", ver)
if ver.API.Version == "2" {
enterpriseEndpoint = reportAPIPathV2
} else {
// If we can't parse the version, we'll assume it's v1 as 4.X does not include the version in the API version response
enterpriseEndpoint = reportAPIPathV1
}

log.Info("Using enterprise endpoint ", enterpriseEndpoint)
return nil
}

func buildURL(anchoreDetails config.AnchoreInfo, version int) (string, error) {
func buildURL(anchoreDetails config.AnchoreInfo, path string) (string, error) {
anchoreURL, err := url.Parse(anchoreDetails.URL)
if err != nil {
return "", err
}

if version == 1 {
anchoreURL.Path += reportAPIPathV1
} else {
anchoreURL.Path += reportAPIPathV2
}
anchoreURL.Path += path

return anchoreURL.String(), nil
}
179 changes: 177 additions & 2 deletions pkg/reporter/reporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"testing"

"github.com/anchore/k8s-inventory/internal/config"
"github.com/anchore/k8s-inventory/pkg/inventory"
"github.com/h2non/gock"
"github.com/stretchr/testify/assert"
)

func TestBuildUrl(t *testing.T) {
Expand All @@ -14,14 +17,186 @@ func TestBuildUrl(t *testing.T) {
}

expectedURL := "https://ancho.re/v1/enterprise/kubernetes-inventory"
actualURL, err := buildURL(anchoreDetails, 1)
actualURL, err := buildURL(anchoreDetails, "v1/enterprise/kubernetes-inventory")
if err != nil || expectedURL != actualURL {
t.Errorf("Failed to build URL:\nexpected=%s\nactual=%s", expectedURL, actualURL)
}

expectedURL = "https://ancho.re/v2/kubernetes-inventory"
actualURL, err = buildURL(anchoreDetails, 2)
actualURL, err = buildURL(anchoreDetails, "v2/kubernetes-inventory")
if err != nil || expectedURL != actualURL {
t.Errorf("Failed to build URL:\nexpected=%s\nactual=%s", expectedURL, actualURL)
}
}

func TestPost(t *testing.T) {
defer gock.Off()

type args struct {
report inventory.Report
anchoreDetails config.AnchoreInfo
}
tests := []struct {
name string
args args
wantErr bool
expectedAPIPath string
}{
{
name: "default post to v2",
args: args{
report: inventory.Report{},
anchoreDetails: config.AnchoreInfo{
URL: "https://ancho.re",
User: "admin",
Password: "foobar",
Account: "test",
HTTP: config.HTTPConfig{
TimeoutSeconds: 10,
Insecure: true,
},
},
},
wantErr: false,
expectedAPIPath: reportAPIPathV2,
},
{
name: "post to v1 when v2 is not found",
args: args{
report: inventory.Report{},
anchoreDetails: config.AnchoreInfo{
URL: "https://ancho.re",
User: "admin",
Password: "foobar",
Account: "test",
HTTP: config.HTTPConfig{
TimeoutSeconds: 10,
Insecure: true,
},
},
},
wantErr: false,
expectedAPIPath: reportAPIPathV1,
},
{
name: "error when v1 and v2 are not found",
args: args{
report: inventory.Report{},
anchoreDetails: config.AnchoreInfo{
URL: "https://ancho.re",
User: "admin",
Password: "foobar",
Account: "test",
HTTP: config.HTTPConfig{
TimeoutSeconds: 10,
Insecure: true,
},
},
},
wantErr: true,
expectedAPIPath: reportAPIPathV1,
},
}
for _, tt := range tests {
switch tt.name {
case "default post to v2":
gock.New("https://ancho.re").
Post(reportAPIPathV2).
Reply(200)
case "post to v1 when v2 is not found":
gock.New("https://ancho.re").
Post(reportAPIPathV2).
Reply(404)
gock.New("https://ancho.re").
Post(reportAPIPathV1).
Reply(200)
gock.New("https://ancho.re").
Get("/version").
Reply(200).
JSON(map[string]interface{}{
"api": map[string]interface{}{},
"db": map[string]interface{}{"schema_version": "400"},
"service": map[string]interface{}{"version": "4.8.0"},
})
case "error when v1 and v2 are not found":
gock.New("https://ancho.re").
Post(reportAPIPathV2).
Reply(404)
gock.New("https://ancho.re").
Get("/version").
Reply(404)
}

t.Run(tt.name, func(t *testing.T) {
// Reset enterpriseEndpoint to the default each test run
enterpriseEndpoint = reportAPIPathV2

err := Post(tt.args.report, tt.args.anchoreDetails)

if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedAPIPath, enterpriseEndpoint)
}
})
}
}

// Simulate a handover from Enterprise 4.x to 5.x
// In this case v1 should be used initially instead of v2 then when v1 is no longer available v2 should be used
func TestPostSimulateV1ToV2HandoverFromEnterprise4Xto5X(t *testing.T) {
defer gock.Off()

testReport := inventory.Report{}
testAnchoreDetails := config.AnchoreInfo{
URL: "https://ancho.re",
User: "admin",
Password: "foobar",
Account: "test",
HTTP: config.HTTPConfig{
TimeoutSeconds: 10,
Insecure: true,
},
}

enterpriseEndpoint = reportAPIPathV2

// After the first post to default v2, the enterpriseEndpoint should be set to v1
gock.New("https://ancho.re").
Post(reportAPIPathV2).
Reply(404)
gock.New("https://ancho.re").
Get("/version").
Reply(200).
JSON(map[string]interface{}{
"api": map[string]interface{}{},
"db": map[string]interface{}{"schema_version": "400"},
"service": map[string]interface{}{"version": "4.8.0"},
})
gock.New("https://ancho.re").
Post(reportAPIPathV1).
Reply(200)
err := Post(testReport, testAnchoreDetails)
assert.NoError(t, err)
assert.Equal(t, reportAPIPathV1, enterpriseEndpoint)

// Simulate upgrade to Enterprise 5.x, v1 should no longer be available
gock.New("https://ancho.re").
Post(reportAPIPathV1).
Reply(404)
gock.New("https://ancho.re").
Get("/version").
Reply(200).
JSON(map[string]interface{}{
"api": map[string]interface{}{"version": "2"},
"db": map[string]interface{}{"schema_version": "400"},
"service": map[string]interface{}{"version": "4.8.0"},
})
gock.New("https://ancho.re").
Post(reportAPIPathV2).
Reply(200)
err = Post(testReport, testAnchoreDetails)
assert.NoError(t, err)
assert.Equal(t, reportAPIPathV2, enterpriseEndpoint)
}

0 comments on commit 8ae17e5

Please sign in to comment.