0565938956
This change pulls as much pagination logic out of the database implementation as possible. Database implementations should now be able to marshal whatever state they need into opaque tokens with the utilities in the pagination package.
264 lines
8.2 KiB
Go
264 lines
8.2 KiB
Go
// Copyright 2017 clair authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package v3
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"golang.org/x/net/context"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
|
|
"github.com/coreos/clair"
|
|
pb "github.com/coreos/clair/api/v3/clairpb"
|
|
"github.com/coreos/clair/database"
|
|
"github.com/coreos/clair/pkg/commonerr"
|
|
"github.com/coreos/clair/pkg/pagination"
|
|
)
|
|
|
|
// NotificationServer implements NotificationService interface for serving RPC.
|
|
type NotificationServer struct {
|
|
Store database.Datastore
|
|
}
|
|
|
|
// AncestryServer implements AncestryService interface for serving RPC.
|
|
type AncestryServer struct {
|
|
Store database.Datastore
|
|
}
|
|
|
|
// StatusServer implements StatusService interface for serving RPC.
|
|
type StatusServer struct {
|
|
Store database.Datastore
|
|
}
|
|
|
|
// GetStatus implements getting the current status of Clair via the Clair service.
|
|
func (s *StatusServer) GetStatus(ctx context.Context, req *pb.GetStatusRequest) (*pb.GetStatusResponse, error) {
|
|
if clairStatus, err := GetClairStatus(s.Store); err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
} else {
|
|
return &pb.GetStatusResponse{Status: clairStatus}, nil
|
|
}
|
|
}
|
|
|
|
// PostAncestry implements posting an ancestry via the Clair gRPC service.
|
|
func (s *AncestryServer) PostAncestry(ctx context.Context, req *pb.PostAncestryRequest) (*pb.PostAncestryResponse, error) {
|
|
ancestryName := req.GetAncestryName()
|
|
if ancestryName == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "ancestry name should not be empty")
|
|
}
|
|
|
|
layers := req.GetLayers()
|
|
if len(layers) == 0 {
|
|
return nil, status.Error(codes.InvalidArgument, "ancestry should have at least one layer")
|
|
}
|
|
|
|
ancestryFormat := req.GetFormat()
|
|
if ancestryFormat == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "ancestry format should not be empty")
|
|
}
|
|
|
|
ancestryLayers := []clair.LayerRequest{}
|
|
for _, layer := range layers {
|
|
if layer == nil {
|
|
err := status.Error(codes.InvalidArgument, "ancestry layer is invalid")
|
|
return nil, err
|
|
}
|
|
|
|
if layer.GetHash() == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "ancestry layer hash should not be empty")
|
|
}
|
|
|
|
if layer.GetPath() == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "ancestry layer path should not be empty")
|
|
}
|
|
|
|
ancestryLayers = append(ancestryLayers, clair.LayerRequest{
|
|
Hash: layer.Hash,
|
|
Headers: layer.Headers,
|
|
Path: layer.Path,
|
|
})
|
|
}
|
|
|
|
err := clair.ProcessAncestry(s.Store, ancestryFormat, ancestryName, ancestryLayers)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, "ancestry is failed to be processed: "+err.Error())
|
|
}
|
|
|
|
clairStatus, err := GetClairStatus(s.Store)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
|
|
return &pb.PostAncestryResponse{Status: clairStatus}, nil
|
|
}
|
|
|
|
// GetAncestry implements retrieving an ancestry via the Clair gRPC service.
|
|
func (s *AncestryServer) GetAncestry(ctx context.Context, req *pb.GetAncestryRequest) (*pb.GetAncestryResponse, error) {
|
|
var (
|
|
respAncestry *pb.GetAncestryResponse_Ancestry
|
|
name = req.GetAncestryName()
|
|
)
|
|
|
|
if name == "" {
|
|
return nil, status.Errorf(codes.InvalidArgument, "ancestry name should not be empty")
|
|
}
|
|
|
|
tx, err := s.Store.Begin()
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
if req.GetWithFeatures() || req.GetWithVulnerabilities() {
|
|
ancestry, ok, err := tx.FindAncestryWithContent(name)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
|
|
if !ok {
|
|
return nil, status.Error(codes.NotFound, fmt.Sprintf("requested ancestry '%s' is not found", req.GetAncestryName()))
|
|
}
|
|
|
|
respAncestry = &pb.GetAncestryResponse_Ancestry{
|
|
Name: name,
|
|
ScannedDetectors: ancestry.ProcessedBy.Detectors,
|
|
ScannedListers: ancestry.ProcessedBy.Listers,
|
|
}
|
|
|
|
for _, layer := range ancestry.Layers {
|
|
ancestryLayer := &pb.GetAncestryResponse_AncestryLayer{
|
|
Layer: &pb.Layer{
|
|
Hash: layer.Hash,
|
|
},
|
|
}
|
|
|
|
if req.GetWithVulnerabilities() {
|
|
featureVulnerabilities, err := tx.FindAffectedNamespacedFeatures(layer.DetectedFeatures)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
|
|
for _, fv := range featureVulnerabilities {
|
|
// Ensure that every feature can be found.
|
|
if !fv.Valid {
|
|
return nil, status.Error(codes.Internal, "ancestry feature is not found")
|
|
}
|
|
|
|
feature := pb.NamespacedFeatureFromDatabaseModel(fv.NamespacedFeature)
|
|
for _, v := range fv.AffectedBy {
|
|
vuln, err := pb.VulnerabilityWithFixedInFromDatabaseModel(v)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
feature.Vulnerabilities = append(feature.Vulnerabilities, vuln)
|
|
}
|
|
ancestryLayer.DetectedFeatures = append(ancestryLayer.DetectedFeatures, feature)
|
|
}
|
|
} else {
|
|
for _, dbFeature := range layer.DetectedFeatures {
|
|
ancestryLayer.DetectedFeatures = append(ancestryLayer.DetectedFeatures, pb.NamespacedFeatureFromDatabaseModel(dbFeature))
|
|
}
|
|
}
|
|
|
|
respAncestry.Layers = append(respAncestry.Layers, ancestryLayer)
|
|
}
|
|
} else {
|
|
dbAncestry, ok, err := tx.FindAncestry(name)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
} else if !ok {
|
|
return nil, status.Error(codes.NotFound, fmt.Sprintf("requested ancestry '%s' is not found", req.GetAncestryName()))
|
|
}
|
|
respAncestry = pb.AncestryFromDatabaseModel(dbAncestry)
|
|
}
|
|
|
|
clairStatus, err := GetClairStatus(s.Store)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
|
|
return &pb.GetAncestryResponse{
|
|
Status: clairStatus,
|
|
Ancestry: respAncestry,
|
|
}, nil
|
|
}
|
|
|
|
// GetNotification implements retrieving a notification via the Clair gRPC
|
|
// service.
|
|
func (s *NotificationServer) GetNotification(ctx context.Context, req *pb.GetNotificationRequest) (*pb.GetNotificationResponse, error) {
|
|
if req.GetName() == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "notification name should not be empty")
|
|
}
|
|
|
|
if req.GetLimit() <= 0 {
|
|
return nil, status.Error(codes.InvalidArgument, "notification page limit should not be empty or less than 1")
|
|
}
|
|
|
|
tx, err := s.Store.Begin()
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
dbNotification, ok, err := tx.FindVulnerabilityNotification(
|
|
req.GetName(),
|
|
int(req.GetLimit()),
|
|
pagination.Token(req.GetOldVulnerabilityPage()),
|
|
pagination.Token(req.GetNewVulnerabilityPage()),
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
|
|
if !ok {
|
|
return nil, status.Error(codes.NotFound, fmt.Sprintf("requested notification '%s' is not found", req.GetName()))
|
|
}
|
|
|
|
notification, err := pb.NotificationFromDatabaseModel(dbNotification)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
|
|
return &pb.GetNotificationResponse{Notification: notification}, nil
|
|
}
|
|
|
|
// MarkNotificationAsRead implements deleting a notification via the Clair gRPC
|
|
// service.
|
|
func (s *NotificationServer) MarkNotificationAsRead(ctx context.Context, req *pb.MarkNotificationAsReadRequest) (*pb.MarkNotificationAsReadResponse, error) {
|
|
if req.GetName() == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "notification name should not be empty")
|
|
}
|
|
|
|
tx, err := s.Store.Begin()
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
|
|
defer tx.Rollback()
|
|
err = tx.DeleteNotification(req.GetName())
|
|
if err == commonerr.ErrNotFound {
|
|
return nil, status.Error(codes.NotFound, "requested notification \""+req.GetName()+"\" is not found")
|
|
} else if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
|
|
return &pb.MarkNotificationAsReadResponse{}, nil
|
|
}
|