diff --git a/apiv2/internal/server/os_update_run.go b/apiv2/internal/server/os_update_run.go index 69de44e7b..79c886a2d 100644 --- a/apiv2/internal/server/os_update_run.go +++ b/apiv2/internal/server/os_update_run.go @@ -5,31 +5,164 @@ package server import ( "context" + "time" "google.golang.org/grpc/codes" + "google.golang.org/protobuf/types/known/timestamppb" computev1 "github.com/open-edge-platform/infra-core/apiv2/v2/internal/pbapi/resources/compute/v1" + statusv1 "github.com/open-edge-platform/infra-core/apiv2/v2/internal/pbapi/resources/status/v1" restv1 "github.com/open-edge-platform/infra-core/apiv2/v2/internal/pbapi/services/v1" + inv_computev1 "github.com/open-edge-platform/infra-core/inventory/v2/pkg/api/compute/v1" + inventory "github.com/open-edge-platform/infra-core/inventory/v2/pkg/api/inventory/v1" "github.com/open-edge-platform/infra-core/inventory/v2/pkg/errors" + "github.com/open-edge-platform/infra-core/inventory/v2/pkg/validator" ) -func (is *InventorygRPCServer) ListOSUpdateRun(_ context.Context, _ *restv1.ListOSUpdateRunRequest) ( +func parseTimestamp(ts string) (*timestamppb.Timestamp, error) { + if ts == "" { + zlog.Warn().Msgf("timestamp is empty") + return nil, errors.Errorfc( + codes.InvalidArgument, "timestamp is empty", + ) + } + parsedTime, err := time.Parse(ISO8601TimeFormat, ts) + if err != nil { + zlog.Warn().Err(err).Msgf("Failed to parse timestamp: %s", ts) + return nil, err + } + return timestamppb.New(parsedTime), nil +} + +func fromInvOSUpdateRunResource(invOSUpdateRunResource *inv_computev1.OSUpdateRunResource) (*computev1.OSUpdateRun, error) { + if invOSUpdateRunResource == nil { + return &computev1.OSUpdateRun{}, nil + } + instance, err := fromInvInstance(invOSUpdateRunResource.GetInstance()) + if err != nil { + zlog.Warn().Err(err).Msgf("Failed to get the inventory instance edge from OS Update Run resource") + return nil, err + } + invStatusTimestamp, err := parseTimestamp(invOSUpdateRunResource.GetStatusTimestamp()) + if err != nil && err.Error() != errors.Errorfc(codes.InvalidArgument, "timestamp is empty").Error() { + zlog.Warn().Msgf("Status timestamp parsing failed for OS Update Run resource: %s", invOSUpdateRunResource.GetResourceId()) + return nil, errors.Errorfc( + codes.InvalidArgument, "status timestamp parsing failed for OS Update Run resource: %s", + invOSUpdateRunResource.GetResourceId(), + ) + } + invStartTime, err := parseTimestamp(invOSUpdateRunResource.GetStartTime()) + if err != nil && err.Error() != errors.Errorfc(codes.InvalidArgument, "timestamp is empty").Error() { + zlog.Warn().Msgf("Start time parsing failed for OS Update Run resource: %s", invOSUpdateRunResource.GetResourceId()) + return nil, errors.Errorfc( + codes.InvalidArgument, "start time parsing failed for OS Update Run resource: %s", + invOSUpdateRunResource.GetResourceId(), + ) + } + invEndTime, err := parseTimestamp(invOSUpdateRunResource.GetEndTime()) + if err != nil && err.Error() != errors.Errorfc(codes.InvalidArgument, "timestamp is empty").Error() { + zlog.Warn().Msgf("End time parsing failed for OS Update Run resource: %s", invOSUpdateRunResource.GetResourceId()) + return nil, errors.Errorfc( + codes.InvalidArgument, "end time parsing failed for OS Update Run resource: %s", + invOSUpdateRunResource.GetResourceId(), + ) + } + + osUpdateRunResource := &computev1.OSUpdateRun{ + ResourceId: invOSUpdateRunResource.GetResourceId(), + Name: invOSUpdateRunResource.GetName(), + Description: invOSUpdateRunResource.GetDescription(), + AppliedPolicy: fromInvOSUpdatePolicy(invOSUpdateRunResource.GetAppliedPolicy()), + Instance: instance, + StatusIndicator: statusv1.StatusIndication(invOSUpdateRunResource.GetStatusIndicator()), + Status: invOSUpdateRunResource.GetStatus(), + StatusDetails: invOSUpdateRunResource.GetStatusDetails(), + StatusTimestamp: invStatusTimestamp, + StartTime: invStartTime, + EndTime: invEndTime, + Timestamps: GrpcToOpenAPITimestamps(invOSUpdateRunResource), + } + return osUpdateRunResource, nil +} + +func (is *InventorygRPCServer) ListOSUpdateRun(ctx context.Context, req *restv1.ListOSUpdateRunRequest) ( *restv1.ListOSUpdateRunResponse, error, ) { - // TODO implement me - return nil, errors.Errorfc(codes.Unimplemented, "ListOSUpdateRun not implemented") + zlog.Debug().Msg("ListOSUpdateRunResources") + + filter := &inventory.ResourceFilter{ + Resource: &inventory.Resource{ + Resource: &inventory.Resource_OsUpdateRun{ + OsUpdateRun: &inv_computev1.OSUpdateRunResource{}, + }, + }, + Offset: req.GetOffset(), + Limit: req.GetPageSize(), + OrderBy: req.GetOrderBy(), + Filter: req.GetFilter(), + } + if err := validator.ValidateMessage(filter); err != nil { + zlog.InfraSec().InfraErr(err).Msg("failed to validate query params") + return nil, errors.Wrap(err) + } + invResp, err := is.InvClient.List(ctx, filter) + if err != nil { + zlog.InfraErr(err).Msg("Failed to list OS resources from inventory") + return nil, errors.Wrap(err) + } + + osUpdateRunResources := []*computev1.OSUpdateRun{} + for _, invRes := range invResp.GetResources() { + osUpdateRunResource, err := fromInvOSUpdateRunResource(invRes.GetResource().GetOsUpdateRun()) + if err != nil { + zlog.InfraErr(err).Msgf("Failed to convert inventory OS Update Run resource %s", + invRes.GetResource().GetOsUpdateRun().GetResourceId()) + return nil, errors.Wrap(err) + } + osUpdateRunResources = append(osUpdateRunResources, osUpdateRunResource) + } + + resp := &restv1.ListOSUpdateRunResponse{ + OsUpdateRuns: osUpdateRunResources, + TotalElements: invResp.GetTotalElements(), + HasNext: invResp.GetHasNext(), + } + zlog.Debug().Msgf("Listed %s", resp) + return resp, nil } -func (is *InventorygRPCServer) GetOSUpdateRun(_ context.Context, _ *restv1.GetOSUpdateRunRequest) ( +func (is *InventorygRPCServer) GetOSUpdateRun(ctx context.Context, req *restv1.GetOSUpdateRunRequest) ( *computev1.OSUpdateRun, error, ) { - // TODO implement me - return nil, errors.Errorfc(codes.Unimplemented, "GetOSUpdateRun not implemented") + zlog.Debug().Msg("GetOSUpdateRunResource") + + invResp, err := is.InvClient.Get(ctx, req.GetResourceId()) + if err != nil { + zlog.InfraErr(err).Msg("Failed to get OS Update Run resource from inventory") + return nil, errors.Wrap(err) + } + + invOSUpdateRunResource := invResp.GetResource().GetOsUpdateRun() + osUpdateRunResource, err := fromInvOSUpdateRunResource(invOSUpdateRunResource) + if err != nil { + zlog.InfraErr(err).Msgf("Failed to convert inventory OS Update Run resource %s", invOSUpdateRunResource.GetResourceId()) + return nil, errors.Wrap(err) + } + + zlog.Debug().Msgf("Got %s", osUpdateRunResource) + return osUpdateRunResource, nil } -func (is *InventorygRPCServer) DeleteOSUpdateRun(_ context.Context, _ *restv1.DeleteOSUpdateRunRequest) ( +func (is *InventorygRPCServer) DeleteOSUpdateRun(ctx context.Context, req *restv1.DeleteOSUpdateRunRequest) ( *restv1.DeleteOSUpdateRunResponse, error, ) { - // TODO implement me - return nil, errors.Errorfc(codes.Unimplemented, "DeleteOSUpdateRun not implemented") + zlog.Debug().Msg("DeleteOSUpdateRunResource") + + _, err := is.InvClient.Delete(ctx, req.GetResourceId()) + if err != nil { + zlog.InfraErr(err).Msg("Failed to delete OS Update Run resource from inventory") + return nil, errors.Wrap(err) + } + zlog.Debug().Msgf("Deleted %s", req.GetResourceId()) + return &restv1.DeleteOSUpdateRunResponse{}, nil } diff --git a/apiv2/internal/server/os_update_run_test.go b/apiv2/internal/server/os_update_run_test.go index c67edcf7d..504c0f032 100644 --- a/apiv2/internal/server/os_update_run_test.go +++ b/apiv2/internal/server/os_update_run_test.go @@ -5,42 +5,250 @@ package server_test import ( "context" + "errors" "testing" + "time" - "github.com/stretchr/testify/assert" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" + "github.com/stretchr/testify/mock" + computev1 "github.com/open-edge-platform/infra-core/apiv2/v2/internal/pbapi/resources/compute/v1" restv1 "github.com/open-edge-platform/infra-core/apiv2/v2/internal/pbapi/services/v1" invserver "github.com/open-edge-platform/infra-core/apiv2/v2/internal/server" + inv_computev1 "github.com/open-edge-platform/infra-core/inventory/v2/pkg/api/compute/v1" + inventory "github.com/open-edge-platform/infra-core/inventory/v2/pkg/api/inventory/v1" +) + +// Example resources for testing. +var ( + exampleAPIOSUpdateRunResource = &computev1.OSUpdateRun{ + ResourceId: "osupdaterun-12345678", + Name: "example-run", + Description: "An example OS update run", + } + exampleInvOSUpdateRunResource = &inv_computev1.OSUpdateRunResource{ + ResourceId: "osupdaterun-12345678", + Name: "example-run", + Description: "An example OS update run", + StartTime: time.Now().UTC().Format(invserver.ISO8601TimeFormat), + } ) func TestListOSUpdateRun(t *testing.T) { mockedClient := newMockedInventoryTestClient() server := invserver.InventorygRPCServer{InvClient: mockedClient} - _, err := server.ListOSUpdateRun(context.Background(), &restv1.ListOSUpdateRunRequest{}) - assert.Error(t, err) - assert.Equal(t, codes.Unimplemented, status.Code(err)) - assert.Contains(t, err.Error(), "ListOSUpdateRun not implemented") + cases := []struct { + name string + mocks func() []*mock.Call + ctx context.Context + req *restv1.ListOSUpdateRunRequest + wantErr bool + }{ + { + name: "List OSUpdateRun", + mocks: func() []*mock.Call { + return []*mock.Call{ + mockedClient.On("List", mock.Anything, mock.Anything). + Return(&inventory.ListResourcesResponse{ + Resources: []*inventory.GetResourceResponse{ + { + Resource: &inventory.Resource{ + Resource: &inventory.Resource_OsUpdateRun{ + OsUpdateRun: exampleInvOSUpdateRunResource, + }, + }, + }, + }, + TotalElements: 1, + HasNext: false, + }, nil).Once(), + } + }, + ctx: context.Background(), + req: &restv1.ListOSUpdateRunRequest{ + PageSize: 10, + Offset: 0, + }, + wantErr: false, + }, + { + name: "List OSUpdateRun with error", + mocks: func() []*mock.Call { + return []*mock.Call{ + mockedClient.On("List", mock.Anything, mock.Anything). + Return(nil, errors.New("error")).Once(), + } + }, + ctx: context.Background(), + req: &restv1.ListOSUpdateRunRequest{ + PageSize: 10, + Offset: 0, + }, + wantErr: true, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.mocks != nil { + tc.mocks() + } + + reply, err := server.ListOSUpdateRun(tc.ctx, tc.req) + if tc.wantErr { + if err == nil { + t.Errorf("ListOSUpdateRun() got err = nil, want err") + } + return + } + if err != nil { + t.Errorf("ListOSUpdateRun() got err = %v, want nil", err) + return + } + if reply == nil { + t.Errorf("ListOSUpdateRun() got reply = nil, want non-nil") + return + } + if len(reply.GetOsUpdateRuns()) != 1 { + t.Errorf("ListOSUpdateRun() got %v OSUpdatePolicies, want 1", len(reply.GetOsUpdateRuns())) + } + compareProtoMessages(t, exampleAPIOSUpdateRunResource, reply.GetOsUpdateRuns()[0]) + }) + } } func TestGetOSUpdateRun(t *testing.T) { mockedClient := newMockedInventoryTestClient() server := invserver.InventorygRPCServer{InvClient: mockedClient} - _, err := server.GetOSUpdateRun(context.Background(), &restv1.GetOSUpdateRunRequest{}) - assert.Error(t, err) - assert.Equal(t, codes.Unimplemented, status.Code(err)) - assert.Contains(t, err.Error(), "GetOSUpdateRun not implemented") + cases := []struct { + name string + mocks func() []*mock.Call + ctx context.Context + req *restv1.GetOSUpdateRunRequest + wantErr bool + }{ + { + name: "Get OSUpdateRun", + mocks: func() []*mock.Call { + return []*mock.Call{ + mockedClient.On("Get", mock.Anything, "osupdaterun-12345678"). + Return(&inventory.GetResourceResponse{ + Resource: &inventory.Resource{ + Resource: &inventory.Resource_OsUpdateRun{ + OsUpdateRun: exampleInvOSUpdateRunResource, + }, + }, + }, nil).Once(), + } + }, + ctx: context.Background(), + req: &restv1.GetOSUpdateRunRequest{ + ResourceId: "osupdaterun-12345678", + }, + wantErr: false, + }, + { + name: "Get OSUpdateRun with error", + mocks: func() []*mock.Call { + return []*mock.Call{ + mockedClient.On("Get", mock.Anything, "osupdaterun-12345678"). + Return(nil, errors.New("error")).Once(), + } + }, + ctx: context.Background(), + req: &restv1.GetOSUpdateRunRequest{ + ResourceId: "osupdaterun-12345678", + }, + wantErr: true, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.mocks != nil { + tc.mocks() + } + + reply, err := server.GetOSUpdateRun(tc.ctx, tc.req) + if tc.wantErr { + if err == nil { + t.Errorf("GetOSUpdateRun() got err = nil, want err") + } + return + } + if err != nil { + t.Errorf("GetOSUpdateRun() got err = %v, want nil", err) + return + } + if reply == nil { + t.Errorf("GetOSUpdateRun() got reply = nil, want non-nil") + return + } + compareProtoMessages(t, exampleAPIOSUpdateRunResource, reply) + }) + } } func TestDeleteOSUpdateRun(t *testing.T) { mockedClient := newMockedInventoryTestClient() server := invserver.InventorygRPCServer{InvClient: mockedClient} - _, err := server.DeleteOSUpdateRun(context.Background(), &restv1.DeleteOSUpdateRunRequest{}) - assert.Error(t, err) - assert.Equal(t, codes.Unimplemented, status.Code(err)) - assert.Contains(t, err.Error(), "DeleteOSUpdateRun not implemented") + cases := []struct { + name string + mocks func() []*mock.Call + ctx context.Context + req *restv1.DeleteOSUpdateRunRequest + wantErr bool + }{ + { + name: "Delete OSUpdateRun", + mocks: func() []*mock.Call { + return []*mock.Call{ + mockedClient.On("Delete", mock.Anything, "osupdaterun-12345678"). + Return(&inventory.DeleteResourceResponse{}, nil).Once(), + } + }, + ctx: context.Background(), + req: &restv1.DeleteOSUpdateRunRequest{ + ResourceId: "osupdaterun-12345678", + }, + wantErr: false, + }, + { + name: "Delete OSUpdateRun with error", + mocks: func() []*mock.Call { + return []*mock.Call{ + mockedClient.On("Delete", mock.Anything, "osupdaterun-12345678"). + Return(nil, errors.New("error")).Once(), + } + }, + ctx: context.Background(), + req: &restv1.DeleteOSUpdateRunRequest{ + ResourceId: "osupdaterun-12345678", + }, + wantErr: true, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.mocks != nil { + tc.mocks() + } + + reply, err := server.DeleteOSUpdateRun(tc.ctx, tc.req) + if tc.wantErr { + if err == nil { + t.Errorf("DeleteOSUpdateRun() got err = nil, want err") + } + return + } + if err != nil { + t.Errorf("DeleteOSUpdateRun() got err = %v, want nil", err) + return + } + if reply == nil { + t.Errorf("DeleteOSUpdateRun() got reply = nil, want non-nil") + return + } + }) + } }