Skip to content

Commit

Permalink
client function and api endpoint for firmware validation
Browse files Browse the repository at this point in the history
  • Loading branch information
DoctorVin committed Aug 16, 2024
1 parent 824f714 commit 6f424fc
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 13 deletions.
25 changes: 25 additions & 0 deletions internal/dbtools/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ var (
FixtureEventHistoryServer *models.Server
FixtureEventHistoryRelatedID uuid.UUID
FixtureEventHistories []*models.EventHistory

FixtureFWValidationServer *models.Server
FixtureFWValidationSet *models.ComponentFirmwareSet
)

func addFixtures(t *testing.T) error {
Expand Down Expand Up @@ -183,6 +186,10 @@ func addFixtures(t *testing.T) error {
return err
}

if err := setupFWValidationFixtures(ctx, testDB); err != nil {
return err
}

// excluding Chuckles here since that server is deleted
FixtureServers = models.ServerSlice{FixtureNemo, FixtureDory, FixtureMarlin}
FixtureDeletedServers = models.ServerSlice{FixtureChuckles}
Expand Down Expand Up @@ -886,3 +893,21 @@ func setupConfigSet(ctx context.Context, db *sqlx.DB) error {

return nil
}

func setupFWValidationFixtures(ctx context.Context, db *sqlx.DB) error {
FixtureFWValidationServer = &models.Server{
Name: null.StringFrom("firmware-validation"),
FacilityCode: null.StringFrom("tf2"),
}

if err := FixtureFWValidationServer.Insert(ctx, db, boil.Infer()); err != nil {
return errors.Wrap(err, "firmware validation server fixture")
}

FixtureFWValidationSet = &models.ComponentFirmwareSet{Name: "firmware-validation"}
if err := FixtureFWValidationSet.Insert(ctx, db, boil.Infer()); err != nil {
return errors.Wrap(err, "firmware validation set fixture")
}

return nil
}
6 changes: 6 additions & 0 deletions pkg/api/v1/firmware_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,9 @@ func (sc *ComponentFirmwareSetRequest) toDBModelFirmwareSet() (*models.Component

return s, nil
}

type FirmwareSetValidation struct {
TargetServer uuid.UUID `json:"target_server" binding:"required"`
FirmwareSet uuid.UUID `json:"firmware_set" binding:"required"`
PerformedOn time.Time `json:"performed_on" binding:"required"`
}
7 changes: 7 additions & 0 deletions pkg/api/v1/requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ func (c *Client) do(req *http.Request, result interface{}) error {

defer resp.Body.Close()

switch resp.StatusCode {
case http.StatusNoContent, http.StatusResetContent:
// these statuses are not allowed to have body content
return nil
default:
}

data, err := io.ReadAll(resp.Body)
if err != nil {
return err
Expand Down
25 changes: 19 additions & 6 deletions pkg/api/v1/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,25 @@ func (r *Router) Routes(rg *gin.RouterGroup) {
// /server-component-firmware-sets
srvCmpntFwSets := rg.Group("/server-component-firmware-sets")
{
srvCmpntFwSets.GET("", amw.AuthRequired(readScopes("server-component-firmware-sets")), r.serverComponentFirmwareSetList)
srvCmpntFwSets.POST("", amw.AuthRequired(createScopes("server-component-firmware-sets")), r.serverComponentFirmwareSetCreate)
srvCmpntFwSets.GET("/:uuid", amw.AuthRequired(readScopes("server-component-firmware-sets")), r.serverComponentFirmwareSetGet)
srvCmpntFwSets.PUT("/:uuid", amw.AuthRequired(updateScopes("server-component-firmware-sets")), r.serverComponentFirmwareSetUpdate)
srvCmpntFwSets.DELETE("/:uuid", amw.AuthRequired(deleteScopes("server-component-firmware-sets")), r.serverComponentFirmwareSetDelete)
srvCmpntFwSets.POST("/:uuid/remove-firmware", amw.AuthRequired(deleteScopes("server-component-firmware-sets")), r.serverComponentFirmwareSetRemoveFirmware)
createScopeMiddleware := amw.AuthRequired(createScopes("server-component-firmware-sets"))
readScopeMiddleware := amw.AuthRequired(readScopes("server-component-firmware-sets"))
updateScopeMiddleware := amw.AuthRequired(updateScopes("server-component-firmware-sets"))
deleteScopeMiddleware := amw.AuthRequired(deleteScopes("server-component-firmware-sets"))

// list all sets
srvCmpntFwSets.GET("", readScopeMiddleware, r.serverComponentFirmwareSetList)

// create/read/update/delete individual firmware sets
srvCmpntFwSets.POST("", createScopeMiddleware, r.serverComponentFirmwareSetCreate)
srvCmpntFwSets.GET("/:uuid", readScopeMiddleware, r.serverComponentFirmwareSetGet)
srvCmpntFwSets.PUT("/:uuid", updateScopeMiddleware, r.serverComponentFirmwareSetUpdate)
srvCmpntFwSets.DELETE("/:uuid", deleteScopeMiddleware, r.serverComponentFirmwareSetDelete)

// remove a component firmware from the set
srvCmpntFwSets.POST("/:uuid/remove-firmware", deleteScopeMiddleware, r.serverComponentFirmwareSetRemoveFirmware)

// mark the set as validated
srvCmpntFwSets.POST("/validate-firmware-set", updateScopeMiddleware, r.validateFirmwareSet)
}

// /bill-of-materials
Expand Down
89 changes: 89 additions & 0 deletions pkg/api/v1/router_firmware_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fleetdbapi
import (
"context"
"database/sql"
"net/http"

"fmt"
"strings"
Expand All @@ -13,7 +14,9 @@ import (
"github.com/volatiletech/null/v8"
"github.com/volatiletech/sqlboiler/v4/boil"
"github.com/volatiletech/sqlboiler/v4/queries/qm"
"go.uber.org/zap"

"github.com/metal-toolbox/fleetdb/internal/metrics"
"github.com/metal-toolbox/fleetdb/internal/models"
)

Expand Down Expand Up @@ -698,3 +701,89 @@ func (r *Router) firmwareSetDeleteMappingTx(ctx context.Context, _ *models.Compo

return tx.Commit()
}

// We allow multiple calls for the same firmware-set and server-id because firmware sets are mutable.
// We will update any record in place, or create a new one as needed.
func (r *Router) validateFirmwareSet(c *gin.Context) {
var payload FirmwareSetValidation
if err := c.ShouldBindJSON(&payload); err != nil {
badRequestResponse(c, "invalid validation payload", err)
return
}
ctx := c.Request.Context()
txn, err := r.DB.BeginTx(ctx, nil)
if err != nil {
dbErrorResponse2(c, "starting transaction", err)
return
}

doRollback := false
rollbackFn := func() {
if doRollback {
if rbErr := txn.Rollback(); rbErr != nil {
r.Logger.With(
zap.Error(rbErr),
).Warn("rollback error on firmware validation")
metrics.DBError("rollback firmware validation")
}
}
}
defer rollbackFn()

fact := models.FirmwareSetValidationFact{
TargetServerID: payload.TargetServer.String(),
FirmwareSetID: payload.FirmwareSet.String(),
PerformedOn: payload.PerformedOn,
}

existing, err := models.FirmwareSetValidationFacts(
models.FirmwareSetValidationFactWhere.FirmwareSetID.EQ(payload.FirmwareSet.String()),
).One(ctx, txn)

switch err {
case nil:
fact.ID = existing.ID
_, updErr := fact.Update(ctx, txn, boil.Infer())
if updErr != nil {
r.Logger.With(
zap.Error(updErr),
zap.String("firmware.set", payload.FirmwareSet.String()),
zap.String("target.server", payload.TargetServer.String()),
).Warn("updating existing firmware validation record")
metrics.DBError("update firmware validation")
doRollback = true
dbErrorResponse2(c, "update firmware validation", updErr)
return
}
case sql.ErrNoRows:
writeErr := fact.Insert(ctx, txn, boil.Infer())
if writeErr != nil {
r.Logger.With(
zap.Error(writeErr),
zap.String("firmware.set", payload.FirmwareSet.String()),
zap.String("target.server", payload.TargetServer.String()),
).Warn("inserting existing firmware validation record")
metrics.DBError("insert firmware validation")
doRollback = true
dbErrorResponse2(c, "insert firmware validation", writeErr)
return
}
default:
dbErrorResponse2(c, "checking database for existing", err)
return
}

if txErr := txn.Commit(); txErr != nil {
r.Logger.With(
zap.Error(txErr),
zap.String("firmware.set", payload.FirmwareSet.String()),
zap.String("target.server", payload.TargetServer.String()),
).Warn("commit firmware validation record")
doRollback = true
metrics.DBError("commit firmware validation transaction")
dbErrorResponse2(c, "commit firmware validation transaction", txErr)
return
}

c.JSON(http.StatusNoContent, nil)
}
38 changes: 37 additions & 1 deletion pkg/api/v1/router_firmware_set_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"testing"
"time"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -610,7 +611,7 @@ func TestIntegrationServerComponentFirmwareSetList(t *testing.T) {
},
nil,
nil,
5,
6,
2,
false,
"",
Expand Down Expand Up @@ -979,3 +980,38 @@ func TestIntegrationServerComponentFirmwareSetRemoveFirmware(t *testing.T) {
})
}
}

func TestIntegrationValidateFirmwareSet(t *testing.T) {
s := serverTest(t)

t.Logf("validation set fixture: %v", dbtools.FixtureFWValidationSet)
setID, err := uuid.Parse(dbtools.FixtureFWValidationSet.ID)
require.NoError(t, err)

srvID, err := uuid.Parse(dbtools.FixtureFWValidationServer.ID)
require.NoError(t, err)

realClientTests(t, func(ctx context.Context, authToken string, respCode int, expectError bool) error {
s.Client.SetToken(authToken)

on := time.Now()

err := s.Client.ValidateFirmwareSet(ctx, srvID, setID, on)
if !expectError {
require.NoError(t, err)
}
return err
})

// Updating an existing validation is kosher.
err = s.Client.ValidateFirmwareSet(context.TODO(), srvID, setID, time.Now())
require.NoError(t, err, "update with fixture server id failed")

// Validate the same firmware with a non-fixture server id -- expect success
err = s.Client.ValidateFirmwareSet(context.TODO(), uuid.New(), setID, time.Now())
require.NoError(t, err, "update with unregistered server id failed")

// bogus firmware set id
err = s.Client.ValidateFirmwareSet(context.TODO(), uuid.New(), uuid.New(), time.Now())
require.Error(t, err, "update with bogus firmware set id succeeded")
}
15 changes: 9 additions & 6 deletions pkg/api/v1/router_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ func TestIntegrationServerList(t *testing.T) {
r, resp, err := s.Client.List(ctx, nil)
if !expectError {
require.NoError(t, err)
assert.Len(t, r, 5)
assert.Len(t, r, 6)

assert.EqualValues(t, 5, resp.PageCount)
assert.EqualValues(t, 6, resp.PageCount)
assert.EqualValues(t, 1, resp.TotalPages)
assert.EqualValues(t, 5, resp.TotalRecordCount)
assert.EqualValues(t, 6, resp.TotalRecordCount)
// We returned everything, so we shouldnt have a next page info
assert.Nil(t, resp.Links.Next)
assert.Nil(t, resp.Links.Previous)
Expand Down Expand Up @@ -145,6 +145,7 @@ func TestIntegrationServerList(t *testing.T) {
"empty search filter",
nil,
[]string{
dbtools.FixtureFWValidationServer.ID,
dbtools.FixtureEventHistoryServer.ID,
dbtools.FixtureInventoryServer.ID,
dbtools.FixtureNemo.ID,
Expand Down Expand Up @@ -443,6 +444,7 @@ func TestIntegrationServerList(t *testing.T) {
"search for server without IncludeDeleted defined",
&fleetdbapi.ServerListParams{},
[]string{
dbtools.FixtureFWValidationServer.ID,
dbtools.FixtureEventHistoryServer.ID,
dbtools.FixtureInventoryServer.ID,
dbtools.FixtureNemo.ID,
Expand All @@ -456,6 +458,7 @@ func TestIntegrationServerList(t *testing.T) {
"search for server with IncludeDeleted defined",
&fleetdbapi.ServerListParams{IncludeDeleted: true},
[]string{
dbtools.FixtureFWValidationServer.ID,
dbtools.FixtureEventHistoryServer.ID,
dbtools.FixtureInventoryServer.ID,
dbtools.FixtureNemo.ID,
Expand Down Expand Up @@ -535,7 +538,7 @@ func TestIntegrationServerListPagination(t *testing.T) {

assert.EqualValues(t, 2, resp.PageCount)
assert.EqualValues(t, 3, resp.TotalPages)
assert.EqualValues(t, 5, resp.TotalRecordCount)
assert.EqualValues(t, 6, resp.TotalRecordCount)
// Since we have a next page let's make sure all the links are set
assert.NotNil(t, resp.Links.Next)
assert.Nil(t, resp.Links.Previous)
Expand All @@ -554,13 +557,13 @@ func TestIntegrationServerListPagination(t *testing.T) {
// get the last page
resp, err = s.Client.NextPage(context.TODO(), *resp, &r)
assert.NoError(t, err)
assert.Len(t, r, 1)
assert.Len(t, r, 2)

// we should have followed the cursor so first/previous/next/last links shouldn't be set
// but there is another page so we should have a next cursor link. Total counts are not includes
// cursor pages
assert.EqualValues(t, 3, resp.TotalPages)
assert.EqualValues(t, 5, resp.TotalRecordCount)
assert.EqualValues(t, 6, resp.TotalRecordCount)
assert.NotNil(t, resp.Links.First)
assert.NotNil(t, resp.Links.Previous)
assert.Nil(t, resp.Links.Next)
Expand Down
16 changes: 16 additions & 0 deletions pkg/api/v1/server_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"path"
"time"

"github.com/google/uuid"
rivets "github.com/metal-toolbox/rivets/types"
Expand Down Expand Up @@ -63,6 +64,7 @@ type ClientInterface interface {
ListServerComponentFirmwareSet(context.Context, *ComponentFirmwareSetListParams) ([]ComponentFirmwareSet, *ServerResponse, error)
ListFirmwareSets(context.Context, *ComponentFirmwareSetListParams) ([]ComponentFirmwareSet, *ServerResponse, error)
DeleteServerComponentFirmwareSet(context.Context, uuid.UUID) (*ServerResponse, error)
ValidateFirmwareSet(context.Context, uuid.UUID, uuid.UUID, time.Time) error

GetCredential(context.Context, uuid.UUID, string) (*ServerCredential, *ServerResponse, error)
SetCredential(context.Context, uuid.UUID, string, string) (*ServerResponse, error)
Expand Down Expand Up @@ -372,6 +374,20 @@ func (c *Client) RemoveServerComponentFirmwareSetFirmware(ctx context.Context, f
return c.post(ctx, path, firmwareSet)
}

// ValidateFirmwareSet inserts or updates a record containing facts about the validation of this
// particular firmware set. On a successful execution the API returns 204 (http.StatusNoContent), so
// there is nothing useful to put into a ServerResponse.
func (c *Client) ValidateFirmwareSet(ctx context.Context, srvID, fwSetID uuid.UUID, on time.Time) error {
path := fmt.Sprintf("%s/validate-firmware-set", serverComponentFirmwareSetsEndpoint)
facts := FirmwareSetValidation{
TargetServer: srvID,
FirmwareSet: fwSetID,
PerformedOn: on,
}
_, err := c.post(ctx, path, facts)
return err
}

// GetCredential will return the secret for the secret type for the given server UUID
func (c *Client) GetCredential(ctx context.Context, srvUUID uuid.UUID, secretSlug string) (*ServerCredential, *ServerResponse, error) {
p := path.Join(serversEndpoint, srvUUID.String(), serverCredentialsEndpoint, secretSlug)
Expand Down

0 comments on commit 6f424fc

Please sign in to comment.