Skip to content

Commit

Permalink
updates: implement inventoryGroupDevicesInfo
Browse files Browse the repository at this point in the history
In the context of inventory group details view we want to update devices of an inventory group.
But before that we to need to retrieve validation data and the device uuids related to this group.

FIXES: https://issues.redhat.com/browse/THEEDGE-3539
  • Loading branch information
ldjebran committed Nov 14, 2023
1 parent cab94ee commit d07828a
Show file tree
Hide file tree
Showing 9 changed files with 451 additions and 1 deletion.
3 changes: 3 additions & 0 deletions pkg/dependencies/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"

"github.com/redhatinsights/edge-api/logger"
"github.com/redhatinsights/edge-api/pkg/clients/inventorygroups"
"github.com/redhatinsights/edge-api/pkg/clients/repositories"
kafkacommon "github.com/redhatinsights/edge-api/pkg/common/kafka"
"github.com/redhatinsights/edge-api/pkg/routes/common"
Expand All @@ -30,6 +31,7 @@ type EdgeAPIServices struct {
FilesService services.FilesService
ProducerService kafkacommon.ProducerServiceInterface
ConsumerService kafkacommon.ConsumerServiceInterface
InventoryGroupsService inventorygroups.ClientInterface
RepositoriesService repositories.ClientInterface
Log *log.Entry
}
Expand Down Expand Up @@ -57,6 +59,7 @@ func Init(ctx context.Context) *EdgeAPIServices {
FilesService: services.NewFilesService(log),
ProducerService: kafkacommon.NewProducerService(),
ConsumerService: kafkacommon.NewConsumerService(ctx, log),
InventoryGroupsService: inventorygroups.InitClient(ctx, log),
RepositoriesService: repositories.InitClient(ctx, log),
Log: log,
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/models/updates.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,13 @@ func (ur *UpdateTransaction) BeforeCreate(tx *gorm.DB) error {

return nil
}

// InventoryGroupDevicesUpdateInfo is the inventory group update info
type InventoryGroupDevicesUpdateInfo struct {
GroupUUID string `json:"group_uuid"`
UpdateValid bool `json:"update_valid"`
ImageSetID uint `json:"image_set_id"`
ImageSetsCount int `json:"image_sets_count"`
DevicesCount int `json:"devices_count"`
DevicesUUIDS []string `json:"update_devices_uuids"`
}
10 changes: 10 additions & 0 deletions pkg/models/updates_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,13 @@ type RecipientNotificationAPI struct {
IgnoreUserPreferences bool `json:"ignore_user_preferences" example:"false"` // notification recipient to ignore user preferences
Users []string `json:"users" example:"user-id"` // notification recipient users
} // @name RecipientNotification

// InventoryGroupDevicesUpdateInfoResponseAPI is the inventory group update info
type InventoryGroupDevicesUpdateInfoResponseAPI struct {
GroupUUID string `json:"group_uuid" example:"b579a578-1a6f-48d5-8a45-21f2a656a5d4"` // the inventory group id
UpdateValid bool `json:"update_valid" example:"true"` // whether the inventory group devices update is valid
ImageSetID uint `json:"image_set_id" example:"1024" ` // the image set id common to all inventory group devices
ImageSetsCount int `json:"image_sets_count" example:"1"` // how much image set ids the inventory group devices belongs to
DevicesCount int `json:"devices_count" example:"25"` // the overall count of all devices that belongs to inventory group
DevicesUUID []string `json:"update_devices_uuids" example:"b579a578-1a6f-48d5-8a45-21f2a656a5d4,1abb288d-6d88-4e2d-bdeb-fcc536be58ec"` // the list of devices uuids that belongs to inventory group that are available to update
}
98 changes: 97 additions & 1 deletion pkg/routes/updates.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import (
"time"

"github.com/go-chi/chi"
"github.com/redhatinsights/edge-api/pkg/clients/inventorygroups"
"github.com/redhatinsights/edge-api/pkg/db"
"github.com/redhatinsights/edge-api/pkg/dependencies"
"github.com/redhatinsights/edge-api/pkg/errors"
"github.com/redhatinsights/edge-api/pkg/models"
"github.com/redhatinsights/edge-api/pkg/routes/common"
"github.com/redhatinsights/edge-api/pkg/services"

"github.com/redhatinsights/edge-api/pkg/services/utility"
feature "github.com/redhatinsights/edge-api/unleash/features"
log "github.com/sirupsen/logrus"
)

Expand All @@ -32,6 +34,10 @@ func MakeUpdatesRouter(sub chi.Router) {
r.Get("/update-playbook.yml", GetUpdatePlaybook)
r.Get("/notify", SendNotificationForDevice) // TMP ROUTE TO SEND THE NOTIFICATION
})
sub.Route("/inventory-groups/{GroupUUID}", func(r chi.Router) {
r.Use(InventoryGroupsCtx)
r.Get("/update-info", GetInventoryGroupDevicesUpdateInfo)
})
// TODO: This is for backwards compatibility with the previous route
// Once the frontend starts querying the device
sub.Route("/device/", MakeDevicesRouter)
Expand Down Expand Up @@ -79,6 +85,52 @@ func UpdateCtx(next http.Handler) http.Handler {
})
}

type inventoryGroupContextKeyType string

const inventoryGroupContextKey = inventoryGroupContextKeyType("inventory_group_key")

// InventoryGroupsCtx a handler for updates inventory groups requests
func InventoryGroupsCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
contextServices := dependencies.ServicesFromContext(r.Context())
orgID := readOrgID(w, r, contextServices.Log)
if orgID == "" {
return
}

groupUUID := chi.URLParam(r, "GroupUUID")
if groupUUID == "" {
respondWithAPIError(w, contextServices.Log, errors.NewBadRequest("missing inventory group uuid"))
return
}
inventoryGroup, err := contextServices.InventoryGroupsService.GetGroupByUUID(groupUUID)
if err != nil {
var apiError errors.APIError
switch err {
case inventorygroups.ErrGroupNotFound:
apiError = errors.NewNotFound("inventory group not found")
default:
apiError = errors.NewInternalServerError()
}
respondWithAPIError(w, contextServices.Log, apiError)
return
}
ctx := context.WithValue(r.Context(), inventoryGroupContextKey, inventoryGroup)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

func getInventoryGroup(w http.ResponseWriter, r *http.Request) *inventorygroups.Group {
ctx := r.Context()
ctxServices := dependencies.ServicesFromContext(ctx)
inventoryGroup, ok := ctx.Value(inventoryGroupContextKey).(*inventorygroups.Group)
if !ok {
respondWithAPIError(w, ctxServices.Log, errors.NewNotFound("inventory group not found in context"))
return nil
}
return inventoryGroup
}

// GetUpdatePlaybook returns the playbook for an update transaction
// @Summary returns the playbook yaml file for a system update
// @ID GetUpdatePlaybook
Expand Down Expand Up @@ -486,3 +538,47 @@ func ValidateGetUpdatesFilterParams(next http.Handler) http.Handler {
respondWithJSONBody(w, ctxServices.Log, &errs)
})
}

// GetInventoryGroupDevicesUpdateInfo returns inventory group update info
// @Summary Gets the inventory group update info
// @ID GetInventoryGroupDevicesUpdateInfo
// @Description Gets the inventory group update info
// @Tags Updates (Systems)
// @Accept json
// @Produce json
// @Param GroupUUID path string true "a unique uuid to identify the inventory group"
// @Success 200 {object} models.InventoryGroupDevicesUpdateInfoResponseAPI "The requested inventory group update info"
// @Failure 400 {object} errors.BadRequest "The request sent couldn't be processed"
// @Failure 404 {object} errors.NotFound "The requested inventory group was not found"
// @Failure 500 {object} errors.InternalServerError "There was an internal server error"
// @Failure 501 {object} errors.NewFeatureNotAvailable "the feature is not implemented"
// @Router /inventory-groups/{GroupUUID}/update-info [get]
func GetInventoryGroupDevicesUpdateInfo(w http.ResponseWriter, r *http.Request) {
ctxServices := dependencies.ServicesFromContext(r.Context())
orgID := readOrgID(w, r, ctxServices.Log)
if orgID == "" {
return
}
inventoryGroup := getInventoryGroup(w, r)
if inventoryGroup == nil {
return
}

enforceEdgeGroups := utility.EnforceEdgeGroups(orgID)
if !feature.EdgeParityInventoryGroupsEnabled.IsEnabled() ||
(feature.EdgeParityInventoryGroupsEnabled.IsEnabled() && enforceEdgeGroups) {
// return feature not available when inventory groups feature is not enabled
// or when the inventory groups feature is enabled but the org is enforced to use edge groups
respondWithAPIError(w, ctxServices.Log, errors.NewFeatureNotAvailable("inventory groups feature is not available"))
return
}

inventoryGroupUpdateDevicesInfo, err := ctxServices.UpdateService.InventoryGroupDevicesUpdateInfo(orgID, inventoryGroup.ID)
if err != nil {
ctxServices.Log.WithFields(log.Fields{"error": err.Error(), "group-uuid": inventoryGroup.ID}).Error("error occurred while getting inventory group update validation")
respondWithAPIError(w, ctxServices.Log, errors.NewInternalServerError())
}

w.WriteHeader(http.StatusOK)
respondWithJSONBody(w, ctxServices.Log, inventoryGroupUpdateDevicesInfo)
}
169 changes: 169 additions & 0 deletions pkg/routes/updates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,20 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"

apiError "github.com/redhatinsights/edge-api/pkg/errors"

"github.com/bxcodec/faker/v3"
"github.com/redhatinsights/edge-api/pkg/clients/inventorygroups"
"github.com/redhatinsights/edge-api/pkg/clients/inventorygroups/mock_inventorygroups"
"github.com/redhatinsights/edge-api/pkg/db"
"github.com/redhatinsights/edge-api/pkg/routes/common"
"github.com/redhatinsights/edge-api/pkg/services"
feature "github.com/redhatinsights/edge-api/unleash/features"
"github.com/redhatinsights/platform-go-middlewares/identity"

"github.com/redhatinsights/edge-api/config"
Expand Down Expand Up @@ -1155,3 +1159,168 @@ func TestValidateGetAllUpdatesQueryParameters(t *testing.T) {
}
}
}

func TestInventoryGroupDevicesUpdateInfo(t *testing.T) {

defer func() {
config.Get().Auth = false
}()

// enable auth
config.Get().Auth = true

orgID := faker.UUIDHyphenated()
groupUUID := faker.UUIDHyphenated()
inventoryGroup := inventorygroups.Group{Name: faker.Name(), ID: groupUUID, OrgID: orgID}
expectedError := errors.New("some expected error")
testCases := []struct {
Name string
EnforceEdgeGroups bool
EdgeParityInventoryGroupsEnabled bool
GroupUUID string
ReturnInventoryGroup *inventorygroups.Group
ReturnInventoryGroupError error
ReturnServiceError error
ReturnServiceData *models.InventoryGroupDevicesUpdateInfo
ExpectedHTTPStatus int
ExpectedHTTPErrorMessage string
}{
{
Name: "should return InventoryGroupDevicesUpdateInfo successfully",
EdgeParityInventoryGroupsEnabled: true,
GroupUUID: groupUUID,
ReturnInventoryGroup: &inventoryGroup,
ReturnServiceData: &models.InventoryGroupDevicesUpdateInfo{UpdateValid: true, DevicesUUIDS: []string{faker.UUIDHyphenated()}},
ExpectedHTTPStatus: http.StatusOK,
},
{
Name: "should return bad request error when inventory group not supplied",
GroupUUID: "",
ExpectedHTTPStatus: http.StatusBadRequest,
ExpectedHTTPErrorMessage: "missing inventory group uuid",
},
{
Name: "should return not found error when inventory group not found",
GroupUUID: groupUUID,
ReturnInventoryGroup: nil,
ReturnInventoryGroupError: inventorygroups.ErrGroupNotFound,
ExpectedHTTPStatus: http.StatusNotFound,
ExpectedHTTPErrorMessage: "inventory group not found",
},
{
Name: "should return internal server error when inventory group return unknown error",
GroupUUID: groupUUID,
ReturnInventoryGroup: nil,
ReturnInventoryGroupError: expectedError,
ExpectedHTTPStatus: http.StatusInternalServerError,
},
{
Name: "should return error when inventory groups feature is not in use",
EdgeParityInventoryGroupsEnabled: false,
GroupUUID: groupUUID,
ReturnInventoryGroup: &inventoryGroup,
ReturnInventoryGroupError: nil,
ExpectedHTTPStatus: http.StatusNotImplemented,
ExpectedHTTPErrorMessage: "inventory groups feature is not available",
},
{
Name: "should return error when EdgeGroups is enforced",
EdgeParityInventoryGroupsEnabled: true,
EnforceEdgeGroups: true,
GroupUUID: groupUUID,
ReturnInventoryGroup: &inventoryGroup,
ReturnInventoryGroupError: nil,
ExpectedHTTPStatus: http.StatusNotImplemented,
ExpectedHTTPErrorMessage: "inventory groups feature is not available",
},
{
Name: "should return error when InventoryGroupDevicesUpdateInfo fails",
EdgeParityInventoryGroupsEnabled: true,
GroupUUID: groupUUID,
ReturnInventoryGroup: &inventoryGroup,
ReturnInventoryGroupError: nil,
ReturnServiceError: expectedError,
ExpectedHTTPStatus: http.StatusInternalServerError,
},
}

for _, testCase := range testCases {
t.Run(testCase.Name, func(t *testing.T) {
RegisterTestingT(t)

defer func() {
_ = os.Unsetenv(feature.EnforceEdgeGroups.EnvVar)
_ = os.Unsetenv(feature.EdgeParityInventoryGroupsEnabled.EnvVar)
}()

ctrl := gomock.NewController(t)
defer ctrl.Finish()

if testCase.EnforceEdgeGroups {
err := os.Setenv(feature.EnforceEdgeGroups.EnvVar, "true")
Expect(err).ToNot(HaveOccurred())
}
if testCase.EdgeParityInventoryGroupsEnabled {
err := os.Setenv(feature.EdgeParityInventoryGroupsEnabled.EnvVar, "true")
Expect(err).ToNot(HaveOccurred())
}

var router chi.Router
var edgeAPIServices *dependencies.EdgeAPIServices

mockUpdateService := mock_services.NewMockUpdateServiceInterface(ctrl)
mockInventoryGroupsClient := mock_inventorygroups.NewMockClientInterface(ctrl)

router = chi.NewRouter()
router.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rLog := log.NewEntry(log.StandardLogger())
ctx := r.Context()
ctx = context.WithValue(ctx, identity.Key, identity.XRHID{Identity: identity.Identity{OrgID: orgID}})
edgeAPIServices = &dependencies.EdgeAPIServices{
UpdateService: mockUpdateService,
InventoryGroupsService: mockInventoryGroupsClient,
Log: rLog,
}
ctx = dependencies.ContextWithServices(ctx, edgeAPIServices)

next.ServeHTTP(w, r.WithContext(ctx))
})
})
router.Route("/updates", MakeUpdatesRouter)

req, err := http.NewRequest(
http.MethodGet, fmt.Sprintf("/updates/inventory-groups/%s/update-info", testCase.GroupUUID), nil,
)
Expect(err).ToNot(HaveOccurred())

if testCase.ReturnInventoryGroup != nil || testCase.ReturnInventoryGroupError != nil {
mockInventoryGroupsClient.EXPECT().GetGroupByUUID(testCase.GroupUUID).Return(
testCase.ReturnInventoryGroup, testCase.ReturnInventoryGroupError,
)
}

if testCase.ReturnServiceData != nil || testCase.ReturnServiceError != nil {
mockUpdateService.EXPECT().InventoryGroupDevicesUpdateInfo(orgID, testCase.GroupUUID).Return(
testCase.ReturnServiceData, testCase.ReturnServiceError,
)
}
responseRecorder := httptest.NewRecorder()
router.ServeHTTP(responseRecorder, req)
respBody, err := io.ReadAll(responseRecorder.Body)
Expect(err).ToNot(HaveOccurred())
Expect(string(respBody)).ToNot(BeEmpty())

Expect(responseRecorder.Code).To(Equal(testCase.ExpectedHTTPStatus))
if testCase.ExpectedHTTPStatus == http.StatusOK && testCase.ReturnServiceData != nil {
var responseUpdateInfo models.InventoryGroupDevicesUpdateInfo
err = json.Unmarshal(respBody, &responseUpdateInfo)
Expect(err).ToNot(HaveOccurred())
Expect(responseUpdateInfo.UpdateValid).To(Equal(testCase.ReturnServiceData.UpdateValid))
Expect(responseUpdateInfo.DevicesUUIDS).To(Equal(testCase.ReturnServiceData.DevicesUUIDS))
} else if testCase.ExpectedHTTPErrorMessage != "" {
Expect(string(respBody)).To(ContainSubstring(testCase.ExpectedHTTPErrorMessage))
}
})
}
}
15 changes: 15 additions & 0 deletions pkg/services/mock_services/updates.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit d07828a

Please sign in to comment.