clair/api/v2/rpc.go
Sida Chen a4edf38566 api: v2 api with gRPC and gRPC-gateway
Newly designed API defines Ancestry as a set of layers
and shrinked the api to only the most used apis:
post ancestry, get layer, get notification, delete notification

Fixes #98
2017-06-13 15:58:10 -04:00

257 lines
8.4 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 v2
import (
"fmt"
google_protobuf1 "github.com/golang/protobuf/ptypes/empty"
log "github.com/sirupsen/logrus"
"golang.org/x/net/context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/coreos/clair"
"github.com/coreos/clair/api/token"
pb "github.com/coreos/clair/api/v2/clairpb"
"github.com/coreos/clair/database"
"github.com/coreos/clair/pkg/commonerr"
"github.com/coreos/clair/pkg/tarutil"
)
// NotificationServer implements NotificationService interface for serving RPC.
type NotificationServer struct {
Store database.Datastore
PaginationKey string
}
// AncestryServer implements AncestryService interface for serving RPC.
type AncestryServer struct {
Store database.Datastore
}
// 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, "Failed to provide proper ancestry name")
}
layers := req.GetLayers()
if len(layers) == 0 {
return nil, status.Error(codes.InvalidArgument, "At least one layer should be provided for an ancestry")
}
var currentName, parentName, rootName string
for i, layer := range layers {
if layer == nil {
err := status.Error(codes.InvalidArgument, "Failed to provide layer")
return nil, s.rollBackOnError(err, currentName, rootName)
}
// TODO(keyboardnerd): after altering the database to support ancestry,
// we should use the ancestry name and index as key instead of
// the amalgamation of ancestry name of index
// Hack: layer name is [ancestryName]-[index] except the tail layer,
// tail layer name is [ancestryName]
if i == len(layers)-1 {
currentName = ancestryName
} else {
currentName = fmt.Sprintf("%s-%d", ancestryName, i)
}
// if rootName is unset, this is the first iteration over the layers and
// the current layer is the root of the ancestry
if rootName == "" {
rootName = currentName
}
err := clair.ProcessLayer(s.Store, req.GetFormat(), currentName, parentName, layer.GetPath(), layer.GetHeaders())
if err != nil {
return nil, s.rollBackOnError(err, currentName, rootName)
}
// Now that the current layer is processed, set the parentName for the
// next iteration.
parentName = currentName
}
return &pb.PostAncestryResponse{
EngineVersion: clair.Version,
}, nil
}
// GetAncestry implements retrieving an ancestry via the Clair gRPC service.
func (s *AncestryServer) GetAncestry(ctx context.Context, req *pb.GetAncestryRequest) (*pb.GetAncestryResponse, error) {
if req.GetAncestryName() == "" {
return nil, status.Errorf(codes.InvalidArgument, "invalid get ancestry request")
}
// TODO(keyboardnerd): after altering the database to support ancestry, this
// function is iteratively querying for for r.GetIndex() th parent of the
// requested layer until the indexed layer is found or index is out of bound
// this is a hack and will be replaced with one query
ancestry, features, err := s.getAncestry(req.GetAncestryName(), req.GetWithFeatures(), req.GetWithVulnerabilities())
if err == commonerr.ErrNotFound {
return nil, status.Error(codes.NotFound, err.Error())
} else if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &pb.GetAncestryResponse{
Ancestry: ancestry,
Features: features,
}, 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, "Failed to provide notification name")
}
if req.GetLimit() <= 0 {
return nil, status.Error(codes.InvalidArgument, "Failed to provide page limit")
}
page := database.VulnerabilityNotificationFirstPage
pageToken := req.GetPage()
if pageToken != "" {
err := token.Unmarshal(pageToken, s.PaginationKey, &page)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "Invalid page format %s", err.Error())
}
} else {
pageTokenBytes, err := token.Marshal(page, s.PaginationKey)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "Failed to marshal token: %s", err.Error())
}
pageToken = string(pageTokenBytes)
}
dbNotification, nextPage, err := s.Store.GetNotification(req.GetName(), int(req.GetLimit()), page)
if err == commonerr.ErrNotFound {
return nil, status.Error(codes.NotFound, err.Error())
} else if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
notification, err := pb.NotificationFromDatabaseModel(dbNotification, int(req.GetLimit()), pageToken, nextPage, s.PaginationKey)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &pb.GetNotificationResponse{Notification: notification}, nil
}
// DeleteNotification implements deleting a notification via the Clair gRPC
// service.
func (s *NotificationServer) DeleteNotification(ctx context.Context, req *pb.DeleteNotificationRequest) (*google_protobuf1.Empty, error) {
if req.GetName() == "" {
return nil, status.Error(codes.InvalidArgument, "Failed to provide notification name")
}
err := s.Store.DeleteNotification(req.GetName())
if err == commonerr.ErrNotFound {
return nil, status.Error(codes.NotFound, err.Error())
} else if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &google_protobuf1.Empty{}, nil
}
// rollBackOnError handles server error and rollback whole ancestry insertion if
// any layer failed to be inserted.
func (s *AncestryServer) rollBackOnError(err error, currentLayerName, rootLayerName string) error {
// if the current layer failed to be inserted and it's the root layer,
// then the ancestry is not yet in the database.
if currentLayerName != rootLayerName {
errrb := s.Store.DeleteLayer(rootLayerName)
if errrb != nil {
return status.Errorf(codes.Internal, errrb.Error())
}
log.WithField("layer name", currentLayerName).Warnf("Can't process %s: roll back the ancestry", currentLayerName)
}
if err == tarutil.ErrCouldNotExtract ||
err == tarutil.ErrExtractedFileTooBig ||
err == clair.ErrUnsupported {
return status.Errorf(codes.InvalidArgument, "unprocessable entity %s", err.Error())
}
if _, badreq := err.(*commonerr.ErrBadRequest); badreq {
return status.Error(codes.InvalidArgument, err.Error())
}
return status.Error(codes.Internal, err.Error())
}
// TODO(keyboardnerd): Remove this Legacy compability code once the database is
// revised.
// getAncestry returns an ancestry from database by getting all parents of a
// layer given the layer name, and the layer's feature list if
// withFeature/withVulnerability is turned on.
func (s *AncestryServer) getAncestry(name string, withFeature bool, withVulnerability bool) (ancestry *pb.Ancestry, features []*pb.Feature, err error) {
var (
layers = []*pb.Layer{}
layer database.Layer
)
ancestry = &pb.Ancestry{}
layer, err = s.Store.FindLayer(name, withFeature, withVulnerability)
if err != nil {
return
}
if withFeature {
for _, fv := range layer.Features {
f, e := pb.FeatureFromDatabaseModel(fv, withVulnerability)
if e != nil {
err = e
return
}
features = append(features, f)
}
}
ancestry.Name = name
ancestry.EngineVersion = int32(layer.EngineVersion)
for name != "" {
layer, err = s.Store.FindLayer(name, false, false)
if err != nil {
return
}
if layer.Parent != nil {
name = layer.Parent.Name
} else {
name = ""
}
layers = append(layers, pb.LayerFromDatabaseModel(layer))
}
// reverse layers to make the root layer at the top
for i, j := 0, len(layers)-1; i < j; i, j = i+1, j-1 {
layers[i], layers[j] = layers[j], layers[i]
}
ancestry.Layers = layers
return
}