a4edf38566
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
257 lines
8.4 KiB
Go
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
|
|
}
|