Handler hostcatalogs (#86)

* Created host catalog service.

* Fixing uri generation for create.

* Use newly submitted static host catalog.

* Copying over tests from projects.

* Fix UpdateHostCatalogRequest merging proto.

* Properly use static host catalog prefixes in tests.

* Correct test to verify the expected uri and id prefix is set.

* Add test for unsetting descriptions.

* Correct bad spelling in comment.

* Tweaking variable names.

* Adding Stringer to the catalog type and setting the type on the response.

* Setting up the catalogType to contain more info regarding its type.

* Validate the type field in requests.

* Remove unused method.

* Comment describing invariants around types.

* Wiring in the repo into the static catalog handler.

* Updated comments to the exported methods.

* fix: return the result of handlers.NotFoundError instead of just calling it.

* Fixing typos referencing just hosts instead of host catlogs.

* Update NewService returns an error and a non pointer type.

* Pull "org_id" and "project_id" into constants and the validation of them into the ValidateAncestors.

* Check errors for all Register*HandlerServer calls.

* fix: Remove left over reference that could cause a panic.

* fix: Changing "x := []string{}" to "var x []string"

* fix: Log errors in test cleanup.

* Changing the code to use google.golang.org/protobuf instead of the older github based protobuf library.

* Changing all t.Fatal checks to require.NoError calls.

* Changing unrecognized prefixes to Invalid Argument instead of Not Found.

* fix: toDbUpdateMask now returns error if it was unable to find any valid paths.

* Define CreatedTime, UpdatedTime, and Type at initialization time in toProto.

* Remove unused host catalog from test.

* Change a t.Fatal to a require.NoError that I missed earlier.

* Change shutdown error logging to t.Errorfs.

* Shorten validateX function names.

* Inline API responses.

* Add todo to make a new repo per request.

* Inline regex check for invalid id.

* Change stored repos into repo creating functions which satisfies ICU-223.

* Wrap proto comments to be under 80 characters long.

* Remove the exception on the read only fields that can be set in an Update request.

* Removing out of date comment.

* Use iam.TestScopes instead of making the project myself.

* Renaming files to be consistant with go test standards and resource naming standards.

* Remove TODO related to a task completed in an earlier commit.

* Fix the previous commit which removed the wrong TODO.
This commit is contained in:
Todd Knight authored and GitHub committed 2020-06-05 11:52:59 -07:00
1 parent 7d4993fb61
commit 7f6e78e66e
12 files changed
+1201 -143

No files matched your search

@@ -1025,12 +1025,26 @@ const _ = grpc.SupportPackageIsVersion6
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type HostCatalogServiceClient interface {
// This retrieves the host catalog specified in the request using the basic view.
// GetHostCatalog returns a stored Host Catalog if present. The provided request must include the org, project, and
// catalog id for the host catalog being retrieved. If any of those ids are missing, malformed, or reference a non
// existing resource an error is returned.
GetHostCatalog(ctx context.Context, in *GetHostCatalogRequest, opts ...grpc.CallOption) (*GetHostCatalogResponse, error)
// ListHostCatalogs returns a list of stored HostCatalogs which exist inside the project referenced in the request.
// The request must include the org and project ids for the HostCatalogs being retrieved.
// If any of those ids are missing, malformed, or reference a non existing parent resource an error is returned.
ListHostCatalogs(ctx context.Context, in *ListHostCatalogsRequest, opts ...grpc.CallOption) (*ListHostCatalogsResponse, error)
// CreateHostCatalog creates and stores a HostCatalog in watchtower. The provided request must include the org, and
// project id in which the catalog will be created and the catalog type to be created. If any of these required fields
// are missing, malformed, or reference a non existing parent resource an error is returned.
CreateHostCatalog(ctx context.Context, in *CreateHostCatalogRequest, opts ...grpc.CallOption) (*CreateHostCatalogResponse, error)
// UpdateHostCatalog updates an exist HostCatalog in Watchtower. The provided HostCatalog must not have any read only
// fields set except for potentially the id. The update mask must be provided and include at least 1 mutable
// field. To unset a field's value include that field name in the update mask and don't set the field in the HostCatalog.
UpdateHostCatalog(ctx context.Context, in *UpdateHostCatalogRequest, opts ...grpc.CallOption) (*UpdateHostCatalogResponse, error)
// Deleting a HostCatalog removes all hosts and host sets associated with this Catalog.
// DeleteHostCatalog removes a HostCatalog and all associated hosts and host sets from Watchtower. If the provided
// Org, Project, or HostCatalog ids are malformed or not provided DeleteHostCatalog returns an error. It is not
// an error to call DeleteHostCatalog for an Org, Project, or HostCatalog that does not exist as the response indicates
// if it existed or not.
DeleteHostCatalog(ctx context.Context, in *DeleteHostCatalogRequest, opts ...grpc.CallOption) (*DeleteHostCatalogResponse, error)
}
@@ -1089,12 +1103,26 @@ func (c *hostCatalogServiceClient) DeleteHostCatalog(ctx context.Context, in *De
// HostCatalogServiceServer is the server API for HostCatalogService service.
type HostCatalogServiceServer interface {
// This retrieves the host catalog specified in the request using the basic view.
// GetHostCatalog returns a stored Host Catalog if present. The provided request must include the org, project, and
// catalog id for the host catalog being retrieved. If any of those ids are missing, malformed, or reference a non
// existing resource an error is returned.
GetHostCatalog(context.Context, *GetHostCatalogRequest) (*GetHostCatalogResponse, error)
// ListHostCatalogs returns a list of stored HostCatalogs which exist inside the project referenced in the request.
// The request must include the org and project ids for the HostCatalogs being retrieved.
// If any of those ids are missing, malformed, or reference a non existing parent resource an error is returned.
ListHostCatalogs(context.Context, *ListHostCatalogsRequest) (*ListHostCatalogsResponse, error)
// CreateHostCatalog creates and stores a HostCatalog in watchtower. The provided request must include the org, and
// project id in which the catalog will be created and the catalog type to be created. If any of these required fields
// are missing, malformed, or reference a non existing parent resource an error is returned.
CreateHostCatalog(context.Context, *CreateHostCatalogRequest) (*CreateHostCatalogResponse, error)
// UpdateHostCatalog updates an exist HostCatalog in Watchtower. The provided HostCatalog must not have any read only
// fields set except for potentially the id. The update mask must be provided and include at least 1 mutable
// field. To unset a field's value include that field name in the update mask and don't set the field in the HostCatalog.
UpdateHostCatalog(context.Context, *UpdateHostCatalogRequest) (*UpdateHostCatalogResponse, error)
// Deleting a HostCatalog removes all hosts and host sets associated with this Catalog.
// DeleteHostCatalog removes a HostCatalog and all associated hosts and host sets from Watchtower. If the provided
// Org, Project, or HostCatalog ids are malformed or not provided DeleteHostCatalog returns an error. It is not
// an error to call DeleteHostCatalog for an Org, Project, or HostCatalog that does not exist as the response indicates
// if it existed or not.
DeleteHostCatalog(context.Context, *DeleteHostCatalogRequest) (*DeleteHostCatalogResponse, error)
}
@@ -14,7 +14,10 @@ service HostCatalogService {
description: "The host catalog service allows you to get/mutate Host catalogs of every type."
};
// This retrieves the host catalog specified in the request using the basic view.
// GetHostCatalog returns a stored Host Catalog if present. The provided
// request must include the org, project, and catalog id for the host catalog
//being retrieved. If any of those ids are missing, malformed, or reference a
// non existing resource an error is returned.
rpc GetHostCatalog(GetHostCatalogRequest) returns (GetHostCatalogResponse) {
// Gets the host as specified int he url.
option (google.api.http) = {
@@ -30,6 +33,12 @@ service HostCatalogService {
description: "Get's a single Host Catalog with the provided identifier."
};
}
// ListHostCatalogs returns a list of stored HostCatalogs which exist inside
// the project referenced in the request. The request must include the org
// and project ids for the HostCatalogs being retrieved. If any of those ids
// are missing, malformed, or reference a non existing parent resource an
// error is returned.
rpc ListHostCatalogs(ListHostCatalogsRequest) returns (ListHostCatalogsResponse) {
option (google.api.http) = {
get: "/v1/orgs/{org_id}/projects/{project_id}/host-catalogs"
@@ -41,6 +50,12 @@ service HostCatalogService {
summary: "Gets a list of HostCatalogs"
};
}
// CreateHostCatalog creates and stores a HostCatalog in watchtower. The
// provided request must include the org, and project id in which the catalog
// will be created and the catalog type to be created. If any of these
// required fields are missing, malformed, or reference a non existing parent
// resource an error is returned.
rpc CreateHostCatalog(CreateHostCatalogRequest) returns (CreateHostCatalogResponse) {
option (google.api.http) = {
post: "/v1/orgs/{org_id}/projects/{project_id}/host-catalogs"
@@ -57,6 +72,12 @@ service HostCatalogService {
description: "Creates a single host catalog under the specified org and project"
};
}
// UpdateHostCatalog updates an exist HostCatalog in Watchtower. The
// provided HostCatalog must not have any read only fields set.
// The update mask must be provided and include at least 1 mutable field.
// To unset a field's value include that field name in the update mask
// and don't set the field in the HostCatalog.
rpc UpdateHostCatalog(UpdateHostCatalogRequest) returns (UpdateHostCatalogResponse) {
option (google.api.http) = {
patch: "/v1/orgs/{org_id}/projects/{project_id}/host-catalogs/{id}"
@@ -72,7 +93,12 @@ service HostCatalogService {
summary: "Updates a HostCatalog"
};
}
// Deleting a HostCatalog removes all hosts and host sets associated with this Catalog.
// DeleteHostCatalog removes a HostCatalog and all associated hosts and host
// sets from Watchtower. If the provided Org, Project, or HostCatalog ids
// are malformed or not provided DeleteHostCatalog returns an error. It is
// not an error to call DeleteHostCatalog for an Org, Project, or HostCatalog
// that does not exist as the response indicates if it existed or not.
rpc DeleteHostCatalog(DeleteHostCatalogRequest) returns (DeleteHostCatalogResponse) {
option (google.api.http) = {
delete: "/v1/orgs/{org_id}/projects/{project_id}/host-catalogs/{id}"
+8 -6
View File
@@ -8,6 +8,7 @@ import (
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/sdk/helper/mlock"
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/host/static"
"github.com/hashicorp/watchtower/internal/iam"
)
@@ -19,7 +20,8 @@ type Controller struct {
baseCancel context.CancelFunc
// Repos
IamRepo *iam.Repository
IamRepoFn func() (*iam.Repository, error)
StaticHostRepoFn func() (*static.Repository, error)
}
func New(conf *Config) (*Controller, error) {
@@ -51,12 +53,12 @@ func New(conf *Config) (*Controller, error) {
c.baseContext, c.baseCancel = context.WithCancel(context.Background())
// Set up repo stuff
var err error
dbase := db.New(c.conf.Database)
c.IamRepo, err = iam.NewRepository(dbase, dbase, c.conf.ControllerKMS)
if err != nil {
return nil, fmt.Errorf("unable to create iam repo: %w", err)
c.IamRepoFn = func() (*iam.Repository, error) {
return iam.NewRepository(dbase, dbase, c.conf.ControllerKMS)
}
c.StaticHostRepoFn = func() (*static.Repository, error) {
return static.NewRepository(dbase, dbase, c.conf.ControllerKMS)
}
return c, nil
+26 -9
View File
@@ -3,6 +3,7 @@ package controller
import (
"context"
"encoding/json"
"fmt"
"net/http"
"path"
"path/filepath"
@@ -28,7 +29,7 @@ type HandlerProperties struct {
// Handler returns an http.Handler for the services. This can be used on
// its own to mount the Vault API within another web server.
func (c *Controller) handler(props HandlerProperties) http.Handler {
func (c *Controller) handler(props HandlerProperties) (http.Handler, error) {
// Create the muxer to handle the actual endpoints
mux := http.NewServeMux()
@@ -46,25 +47,41 @@ func (c *Controller) handler(props HandlerProperties) http.Handler {
mux.Handle("/passthrough/", prefixHandler)
}
mux.Handle("/v1/", handleGrpcGateway(c))
h, err := handleGrpcGateway(c)
if err != nil {
return nil, err
}
mux.Handle("/v1/", h)
corsWrappedHandler := wrapHandlerWithCors(mux, props)
commonWrappedHandler := wrapHandlerWithCommonFuncs(corsWrappedHandler, c, props)
return commonWrappedHandler
return commonWrappedHandler, nil
}
func handleGrpcGateway(c *Controller) http.Handler {
func handleGrpcGateway(c *Controller) (http.Handler, error) {
// Register*ServiceHandlerServer methods ignore the passed in ctx. Using the baseContext now just in case this changes
// in the future, at which point we'll want to be using the baseContext.
ctx := c.baseContext
mux := runtime.NewServeMux(runtime.WithProtoErrorHandler(handlers.ErrorHandler(c.logger)))
services.RegisterHostCatalogServiceHandlerServer(ctx, mux, &host_catalogs.Service{})
services.RegisterHostSetServiceHandlerServer(ctx, mux, &host_sets.Service{})
services.RegisterHostServiceHandlerServer(ctx, mux, &hosts.Service{})
services.RegisterProjectServiceHandlerServer(ctx, mux, projects.NewService(c.IamRepo))
hcs, err := host_catalogs.NewService(c.StaticHostRepoFn)
if err != nil {
return nil, fmt.Errorf("failed to create host catalog handler service: %w", err)
}
if err := services.RegisterHostCatalogServiceHandlerServer(ctx, mux, hcs); err != nil {
return nil, fmt.Errorf("failed to register host catalog service handler: %w", err)
}
if err := services.RegisterHostSetServiceHandlerServer(ctx, mux, &host_sets.Service{}); err != nil {
return nil, fmt.Errorf("failed to register host set service handler: %w", err)
}
if err := services.RegisterHostServiceHandlerServer(ctx, mux, &hosts.Service{}); err != nil {
return nil, fmt.Errorf("failed to register host service handler: %w", err)
}
if err := services.RegisterProjectServiceHandlerServer(ctx, mux, projects.NewService(c.IamRepoFn)); err != nil {
return nil, fmt.Errorf("failed to register project service handler: %w", err)
}
return mux
return mux, nil
}
func wrapHandlerWithCommonFuncs(h http.Handler, c *Controller, props HandlerProperties) http.Handler {
+3 -21
View File
@@ -2,31 +2,13 @@ package controller
import (
"fmt"
"net"
"net/http"
"testing"
"github.com/hashicorp/vault/internalshared/configutil"
"github.com/hashicorp/watchtower/internal/cmd/base"
"github.com/hashicorp/watchtower/internal/cmd/config"
)
func TestHandleGrpcGateway(t *testing.T) {
c := &Controller{
conf: &Config{
Server: new(base.Server),
RawConfig: config.New(),
},
}
h := c.handler(HandlerProperties{ListenerConfig: new(configutil.Listener)})
l, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatalf("Couldn't listen: %v", err)
}
defer l.Close()
go func() {
http.Serve(l, h)
}()
c := NewTestController(t, nil)
defer c.Shutdown()
cases := []struct {
name string
@@ -46,7 +28,7 @@ func TestHandleGrpcGateway(t *testing.T) {
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
url := fmt.Sprintf("http://%s/%s", l.Addr(), tc.path)
url := fmt.Sprintf("%s/%s", c.ApiAddrs()[0], tc.path)
resp, err := http.Get(url)
if err != nil {
t.Errorf("Got error: %v when non was expected.", err)
@@ -0,0 +1,407 @@
package host_catalogs
import (
"context"
"fmt"
"regexp"
"strings"
pb "github.com/hashicorp/watchtower/internal/gen/controller/api/resources/hosts"
pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/host/static"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/wrapperspb"
)
const (
orgIdFieldName = "org_id"
projectIdFieldName = "project_id"
)
type catalogType int
const (
unknownType catalogType = iota
staticType
)
func (t catalogType) String() string {
switch t {
case staticType:
return "Static"
}
return "Unknown"
}
func (t catalogType) idPrefix() string {
switch t {
case staticType:
return static.HostCatalogPrefix + "_"
}
return "unknown"
}
func typeFromTypeField(t string) catalogType {
switch {
case strings.EqualFold(strings.TrimSpace(t), staticType.String()):
return staticType
}
return unknownType
}
func typeFromId(id string) catalogType {
switch {
case strings.HasPrefix(id, staticType.idPrefix()):
return staticType
}
return unknownType
}
var (
reInvalidID = regexp.MustCompile("[^A-Za-z0-9]")
// TODO(ICU-28): Find a way to auto update these names and enforce the mappings between wire and storage.
wireToStorageMask = map[string]string{
"name": "Name",
"description": "Description",
}
)
type Service struct {
staticRepoFn func() (*static.Repository, error)
}
var _ pbs.HostCatalogServiceServer = Service{}
// NewService returns a host catalog Service which handles host catalog related requests to watchtower and uses the provided
// repositories for storage and retrieval.
func NewService(repoFn func() (*static.Repository, error)) (Service, error) {
if repoFn == nil {
return Service{}, fmt.Errorf("nil static repository provided")
}
return Service{staticRepoFn: repoFn}, nil
}
func (s Service) ListHostCatalogs(ctx context.Context, req *pbs.ListHostCatalogsRequest) (*pbs.ListHostCatalogsResponse, error) {
return nil, status.Error(codes.Unimplemented, "Requested method is unimplemented for Host Catalogs.")
}
// GetHostCatalog implements the interface pbs.HostCatalogServiceServer.
func (s Service) GetHostCatalog(ctx context.Context, req *pbs.GetHostCatalogRequest) (*pbs.GetHostCatalogResponse, error) {
ct := typeFromId(req.GetId())
if ct == unknownType {
return nil, handlers.InvalidArgumentErrorf("Unknown host catalog type.", []string{"id"})
}
if err := validateGetRequest(req, ct); err != nil {
return nil, err
}
hc, err := s.getFromRepo(ctx, req.GetId())
if err != nil {
return nil, err
}
return &pbs.GetHostCatalogResponse{Item: hc}, nil
}
// CreateHostCatalog implements the interface pbs.HostCatalogServiceServer.
func (s Service) CreateHostCatalog(ctx context.Context, req *pbs.CreateHostCatalogRequest) (*pbs.CreateHostCatalogResponse, error) {
if err := validateCreateRequest(req); err != nil {
return nil, err
}
hc, err := s.createInRepo(ctx, req.GetProjectId(), req.GetItem())
if err != nil {
return nil, err
}
return &pbs.CreateHostCatalogResponse{
Item: hc,
Uri: fmt.Sprintf("orgs/%s/projects/%s/host-catalogs/%s", req.GetOrgId(), req.GetProjectId(), hc.GetId()),
}, nil
}
// UpdateHostCatalog implements the interface pbs.HostCatalogServiceServer.
func (s Service) UpdateHostCatalog(ctx context.Context, req *pbs.UpdateHostCatalogRequest) (*pbs.UpdateHostCatalogResponse, error) {
ct := typeFromId(req.GetId())
if ct == unknownType {
return nil, handlers.InvalidArgumentErrorf("Unknown host catalog type.", []string{"id"})
}
if err := validateUpdateRequest(req, ct); err != nil {
return nil, err
}
hc, err := s.updateInRepo(ctx, req.GetProjectId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem())
if err != nil {
return nil, err
}
return &pbs.UpdateHostCatalogResponse{Item: hc}, nil
}
// DeleteHostCatalog implements the interface pbs.HostCatalogServiceServer.
func (s Service) DeleteHostCatalog(ctx context.Context, req *pbs.DeleteHostCatalogRequest) (*pbs.DeleteHostCatalogResponse, error) {
ct := typeFromId(req.GetId())
if ct == unknownType {
return nil, handlers.InvalidArgumentErrorf("Unknown host catalog type.", []string{"id"})
}
if err := validateDeleteRequest(req, ct); err != nil {
return nil, err
}
existed, err := s.deleteFromRepo(ctx, req.GetId())
if err != nil {
return nil, err
}
return &pbs.DeleteHostCatalogResponse{Existed: existed}, nil
}
func (s Service) getFromRepo(ctx context.Context, id string) (*pb.HostCatalog, error) {
repo, err := s.staticRepoFn()
if err != nil {
return nil, err
}
hc, err := repo.LookupCatalog(ctx, id)
if err != nil {
return nil, err
}
if hc == nil {
return nil, handlers.NotFoundErrorf("Host catalog %q doesn't exist.", id)
}
return toProto(hc), nil
}
func (s Service) createInRepo(ctx context.Context, projId string, item *pb.HostCatalog) (*pb.HostCatalog, error) {
var opts []static.Option
if item.GetName() != nil {
opts = append(opts, static.WithName(item.GetName().GetValue()))
}
if item.GetDescription() != nil {
opts = append(opts, static.WithDescription(item.GetDescription().GetValue()))
}
h, err := static.NewHostCatalog(projId, opts...)
if err != nil {
return nil, status.Errorf(codes.Internal, "Unable to build host catalog for creation: %v.", err)
}
repo, err := s.staticRepoFn()
if err != nil {
return nil, err
}
out, err := repo.CreateCatalog(ctx, h)
if err != nil {
return nil, status.Errorf(codes.Internal, "Unable to create host catalog: %v.", err)
}
if out == nil {
return nil, status.Error(codes.Internal, "Unable to create host catalog but no error returned from repository.")
}
return toProto(out), nil
}
func (s Service) updateInRepo(ctx context.Context, projId, id string, mask []string, item *pb.HostCatalog) (*pb.HostCatalog, error) {
var opts []static.Option
if desc := item.GetDescription(); desc != nil {
opts = append(opts, static.WithDescription(desc.GetValue()))
}
if name := item.GetName(); name != nil {
opts = append(opts, static.WithName(name.GetValue()))
}
h, err := static.NewHostCatalog(projId, opts...)
if err != nil {
return nil, status.Errorf(codes.Internal, "Unable to build host catalog for update: %v.", err)
}
h.PublicId = id
dbMask, err := toDbUpdateMask(mask)
if err != nil {
return nil, err
}
repo, err := s.staticRepoFn()
if err != nil {
return nil, err
}
out, rowsUpdated, err := repo.UpdateCatalog(ctx, h, dbMask)
if err != nil {
return nil, status.Errorf(codes.Internal, "Unable to update host catalog: %v.", err)
}
if rowsUpdated == 0 {
return nil, handlers.NotFoundErrorf("Host catalog %q doesn't exist.", id)
}
return toProto(out), nil
}
func (s Service) deleteFromRepo(ctx context.Context, id string) (bool, error) {
repo, err := s.staticRepoFn()
if err != nil {
return false, err
}
rows, err := repo.DeleteCatalog(ctx, id)
if err != nil {
return false, status.Errorf(codes.Internal, "Unable to delete host: %v.", err)
}
return rows > 0, nil
}
// toDbUpdateMask converts the wire format's FieldMask into a list of strings containing FieldMask paths used
func toDbUpdateMask(paths []string) ([]string, error) {
var dbPaths []string
var invalid []string
for _, p := range paths {
for _, f := range strings.Split(p, ",") {
if dbField, ok := wireToStorageMask[strings.TrimSpace(f)]; ok {
dbPaths = append(dbPaths, dbField)
} else {
invalid = append(invalid, f)
}
}
}
if len(invalid) > 0 {
return nil, handlers.InvalidArgumentErrorf(fmt.Sprintf("Invalid fields passed in update_update mask: %v.", invalid), []string{"update_mask"})
}
if len(dbPaths) == 0 {
return nil, handlers.InvalidArgumentErrorf("No valid paths included in the update mask.", []string{"update_mask"})
}
return dbPaths, nil
}
func toProto(in *static.HostCatalog) *pb.HostCatalog {
out := pb.HostCatalog{
Id: in.GetPublicId(),
Type: &wrapperspb.StringValue{Value: staticType.String()},
CreatedTime: in.GetCreateTime().GetTimestamp(),
UpdatedTime: in.GetUpdateTime().GetTimestamp(),
}
if in.GetDescription() != "" {
out.Description = &wrapperspb.StringValue{Value: in.GetDescription()}
}
if in.GetName() != "" {
out.Name = &wrapperspb.StringValue{Value: in.GetName()}
}
return &out
}
// A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce
// requirements on the structure of the request. They verify that:
// * The path passed in is correctly formatted
// * All required parameters are set
// * There are no conflicting parameters provided
// * The type asserted by the ID and/or field is known
// * If relevant, the type derived from the id prefix matches what is claimed by the type field
func validateGetRequest(req *pbs.GetHostCatalogRequest, ct catalogType) error {
if err := validateAncestors(req); err != nil {
return err
}
if !validId(req.GetId(), ct.idPrefix()) {
return handlers.InvalidArgumentErrorf("Improperly formatted identifier.", []string{"id"})
}
return nil
}
func validateCreateRequest(req *pbs.CreateHostCatalogRequest) error {
if err := validateAncestors(req); err != nil {
return err
}
item := req.GetItem()
if item == nil {
return handlers.InvalidArgumentErrorf("The catalog's fields must be set to something.", []string{"item"})
}
if item.GetType() == nil {
return handlers.InvalidArgumentErrorf("Type must be specified when creating a host catalog.", []string{"type"})
}
if typeFromTypeField(item.GetType().GetValue()) == unknownType {
return handlers.InvalidArgumentErrorf("Provided host catalog type is unknown.", []string{"type"})
}
var immutableFieldsSet []string
if item.GetId() != "" {
immutableFieldsSet = append(immutableFieldsSet, "id")
}
if item.GetCreatedTime() != nil {
immutableFieldsSet = append(immutableFieldsSet, "created_time")
}
if item.GetUpdatedTime() != nil {
immutableFieldsSet = append(immutableFieldsSet, "updated_time")
}
if len(immutableFieldsSet) > 0 {
return handlers.InvalidArgumentErrorf("Cannot specify read only fields at creation time.", immutableFieldsSet)
}
return nil
}
func validateUpdateRequest(req *pbs.UpdateHostCatalogRequest, ct catalogType) error {
if err := validateAncestors(req); err != nil {
return err
}
if !validId(req.GetId(), ct.idPrefix()) {
return handlers.InvalidArgumentErrorf("Improperly formatted identifier.", []string{"id"})
}
if req.GetUpdateMask() == nil {
return handlers.InvalidArgumentErrorf("UpdateMask not provided but is required to update a host.", []string{"update_mask"})
}
item := req.GetItem()
if item == nil {
// It is legitimate for no item to be specified in an update request as it indicates all fields provided in
// the mask will be marked as unset.
return nil
}
if item.GetId() != "" && item.GetId() != req.GetId() {
return handlers.InvalidArgumentErrorf("Id in provided item and url do not match.", []string{"id"})
}
var immutableFieldsSet []string
if item.GetType() != nil {
immutableFieldsSet = append(immutableFieldsSet, "type")
}
if item.GetId() != "" {
immutableFieldsSet = append(immutableFieldsSet, "id")
}
if item.GetCreatedTime() != nil {
immutableFieldsSet = append(immutableFieldsSet, "created_time")
}
if item.GetUpdatedTime() != nil {
immutableFieldsSet = append(immutableFieldsSet, "updated_time")
}
if len(immutableFieldsSet) > 0 {
return handlers.InvalidArgumentErrorf("Cannot specify read only fields at update time.", immutableFieldsSet)
}
return nil
}
func validateDeleteRequest(req *pbs.DeleteHostCatalogRequest, ct catalogType) error {
if err := validateAncestors(req); err != nil {
return err
}
if !validId(req.GetId(), ct.idPrefix()) {
return handlers.InvalidArgumentErrorf("Improperly formatted identifier.", []string{"id"})
}
return nil
}
func validId(id, prefix string) bool {
if !strings.HasPrefix(id, prefix) {
return false
}
id = strings.TrimPrefix(id, prefix)
return !reInvalidID.Match([]byte(id))
}
type ancestorProvider interface {
GetOrgId() string
GetProjectId() string
}
// validateAncestors verifies that the ancestors of this call are set and formatted correctly.
func validateAncestors(r ancestorProvider) error {
if r.GetOrgId() == "" {
return handlers.InvalidArgumentErrorf("Missing organization id.", []string{orgIdFieldName})
}
if r.GetProjectId() == "" {
return handlers.InvalidArgumentErrorf("Missing project id.", []string{projectIdFieldName})
}
var badFormat []string
if !validId(r.GetOrgId(), "o_") {
badFormat = append(badFormat, orgIdFieldName)
}
if !validId(r.GetProjectId(), "p_") {
badFormat = append(badFormat, projectIdFieldName)
}
if len(badFormat) > 0 {
return handlers.InvalidArgumentErrorf("Improperly formatted identifier.", badFormat)
}
return nil
}
@@ -0,0 +1,627 @@
package host_catalogs_test
import (
"context"
"fmt"
"strings"
"testing"
"github.com/golang/protobuf/ptypes"
"github.com/golang/protobuf/ptypes/wrappers"
"github.com/hashicorp/watchtower/internal/db"
pb "github.com/hashicorp/watchtower/internal/gen/controller/api/resources/hosts"
pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/host/static"
"github.com/hashicorp/watchtower/internal/iam"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/host_catalogs"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func createDefaultHostCatalogAndRepo(t *testing.T) (*static.HostCatalog, *iam.Scope, func() (*static.Repository, error)) {
t.Helper()
require := require.New(t)
cleanup, conn, _ := db.TestSetup(t, "postgres")
t.Cleanup(func() {
if err := conn.Close(); err != nil {
t.Errorf("failed closing the gorm db: %v", err)
}
if err := cleanup(); err != nil {
t.Errorf("failed to clean up TestSetup: %v", err)
}
})
_, pRes := iam.TestScopes(t, conn)
wrap := db.TestWrapper(t)
rw := db.New(conn)
repoFn := func() (*static.Repository, error) {
return static.NewRepository(rw, rw, wrap)
}
hc, err := static.NewHostCatalog(pRes.GetPublicId(), static.WithName("default"), static.WithDescription("default"))
require.NoError(err, "Couldn't get new catalog.")
repo, err := repoFn()
require.NoError(err, "Couldn't create static repostitory")
hcRes, err := repo.CreateCatalog(context.Background(), hc)
require.NoError(err, "Couldn't persist new catalog.")
return hcRes, pRes, repoFn
}
func TestGet(t *testing.T) {
t.Parallel()
hc, proj, repo := createDefaultHostCatalogAndRepo(t)
toMerge := &pbs.GetHostCatalogRequest{
OrgId: proj.GetParentId(),
ProjectId: proj.GetPublicId(),
Id: hc.GetPublicId(),
}
pHostCatalog := &pb.HostCatalog{
Id: hc.GetPublicId(),
Name: &wrappers.StringValue{Value: hc.GetName()},
Description: &wrappers.StringValue{Value: hc.GetDescription()},
CreatedTime: hc.CreateTime.GetTimestamp(),
UpdatedTime: hc.UpdateTime.GetTimestamp(),
Type: &wrappers.StringValue{Value: "Static"},
}
cases := []struct {
name string
req *pbs.GetHostCatalogRequest
res *pbs.GetHostCatalogResponse
errCode codes.Code
}{
{
name: "Get an Existing HostCatalog",
req: &pbs.GetHostCatalogRequest{Id: hc.GetPublicId()},
res: &pbs.GetHostCatalogResponse{Item: pHostCatalog},
errCode: codes.OK,
},
{
name: "Get a non existing Host Catalog",
req: &pbs.GetHostCatalogRequest{Id: static.HostCatalogPrefix + "_DoesntExis"},
res: nil,
errCode: codes.NotFound,
},
{
name: "Wrong id prefix",
req: &pbs.GetHostCatalogRequest{Id: "j_1234567890"},
res: nil,
errCode: codes.InvalidArgument,
},
{
name: "space in id",
req: &pbs.GetHostCatalogRequest{Id: static.HostCatalogPrefix + "_1 23456789"},
res: nil,
errCode: codes.InvalidArgument,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
req := proto.Clone(toMerge).(*pbs.GetHostCatalogRequest)
proto.Merge(req, tc.req)
s, err := host_catalogs.NewService(repo)
require.NoError(t, err, "Couldn't create a new host catalog service.")
got, gErr := s.GetHostCatalog(context.Background(), req)
assert.Equal(tc.errCode, status.Code(gErr), "GetHostCatalog(%+v) got error %v, wanted %v", req, gErr, tc.errCode)
assert.True(proto.Equal(got, tc.res), "GetHostCatalog(%q) got response %q, wanted %q", req, got, tc.res)
})
}
}
func TestDelete(t *testing.T) {
t.Parallel()
require := require.New(t)
hc, proj, repo := createDefaultHostCatalogAndRepo(t)
s, err := host_catalogs.NewService(repo)
require.NoError(err, "Couldn't create a new host catalog service.")
cases := []struct {
name string
req *pbs.DeleteHostCatalogRequest
res *pbs.DeleteHostCatalogResponse
errCode codes.Code
}{
{
name: "Delete an Existing HostCatalog",
req: &pbs.DeleteHostCatalogRequest{
OrgId: proj.GetParentId(),
ProjectId: proj.GetPublicId(),
Id: hc.GetPublicId(),
},
res: &pbs.DeleteHostCatalogResponse{
Existed: true,
},
errCode: codes.OK,
},
{
name: "Delete bad id HostCatalog",
req: &pbs.DeleteHostCatalogRequest{
OrgId: proj.GetParentId(),
ProjectId: proj.GetPublicId(),
Id: static.HostCatalogPrefix + "_doesntexis",
},
res: &pbs.DeleteHostCatalogResponse{
Existed: false,
},
errCode: codes.OK,
},
{
name: "Delete bad org id in HostCatalog",
req: &pbs.DeleteHostCatalogRequest{
OrgId: "o_doesntexis",
ProjectId: proj.GetPublicId(),
Id: hc.GetPublicId(),
},
res: &pbs.DeleteHostCatalogResponse{
Existed: false,
},
errCode: codes.OK,
},
{
name: "Delete bad project id in HostCatalog",
req: &pbs.DeleteHostCatalogRequest{
OrgId: proj.GetParentId(),
ProjectId: "p_doesntexis",
Id: hc.GetPublicId(),
},
res: &pbs.DeleteHostCatalogResponse{
Existed: false,
},
errCode: codes.OK,
},
{
name: "Bad org formatting",
req: &pbs.DeleteHostCatalogRequest{
OrgId: "bad_format",
ProjectId: proj.GetPublicId(),
Id: hc.GetPublicId(),
},
res: nil,
errCode: codes.InvalidArgument,
},
{
name: "Bad HostCatalog Id formatting",
req: &pbs.DeleteHostCatalogRequest{
OrgId: proj.GetParentId(),
ProjectId: proj.GetPublicId(),
Id: static.HostCatalogPrefix + "_bad_format",
},
res: nil,
errCode: codes.InvalidArgument,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert := assert.New(t)
got, gErr := s.DeleteHostCatalog(context.Background(), tc.req)
assert.Equal(tc.errCode, status.Code(gErr), "DeleteHostCatalog(%+v) got error %v, wanted %v", tc.req, gErr, tc.errCode)
assert.EqualValuesf(tc.res, got, "DeleteHostCatalog(%q) got response %q, wanted %q", tc.req, got, tc.res)
})
}
}
func TestDelete_twice(t *testing.T) {
t.Parallel()
assert := assert.New(t)
hc, proj, repo := createDefaultHostCatalogAndRepo(t)
s, err := host_catalogs.NewService(repo)
require.NoError(t, err, "Couldn't create a new host catalog service.")
req := &pbs.DeleteHostCatalogRequest{
OrgId: proj.GetParentId(),
ProjectId: proj.GetPublicId(),
Id: hc.GetPublicId(),
}
got, gErr := s.DeleteHostCatalog(context.Background(), req)
assert.NoError(gErr, "First attempt")
assert.True(got.GetExisted(), "Expected existed to be true for the first delete.")
got, gErr = s.DeleteHostCatalog(context.Background(), req)
assert.NoError(gErr, "Second attempt")
assert.False(got.GetExisted(), "Expected existed to be false for the second delete.")
}
func TestCreate(t *testing.T) {
t.Parallel()
require := require.New(t)
assert := assert.New(t)
defaultHc, proj, repo := createDefaultHostCatalogAndRepo(t)
defaultHcCreated, err := ptypes.Timestamp(defaultHc.GetCreateTime().GetTimestamp())
require.NoError(err, "Error converting proto to timestamp.")
toMerge := &pbs.CreateHostCatalogRequest{
OrgId: proj.GetParentId(),
ProjectId: proj.GetPublicId(),
}
cases := []struct {
name string
req *pbs.CreateHostCatalogRequest
res *pbs.CreateHostCatalogResponse
errCode codes.Code
}{
{
name: "Create a valid HostCatalog",
req: &pbs.CreateHostCatalogRequest{Item: &pb.HostCatalog{
Name: &wrappers.StringValue{Value: "name"},
Description: &wrappers.StringValue{Value: "desc"},
Type: &wrappers.StringValue{Value: "Static"},
}},
res: &pbs.CreateHostCatalogResponse{
Uri: fmt.Sprintf("orgs/%s/projects/%s/host-catalogs/%s_", proj.GetParentId(), proj.GetPublicId(), static.HostCatalogPrefix),
Item: &pb.HostCatalog{
Name: &wrappers.StringValue{Value: "name"},
Description: &wrappers.StringValue{Value: "desc"},
Type: &wrappers.StringValue{Value: "Static"},
},
},
errCode: codes.OK,
},
{
name: "Create with unknown type",
req: &pbs.CreateHostCatalogRequest{Item: &pb.HostCatalog{
Name: &wrappers.StringValue{Value: "name"},
Description: &wrappers.StringValue{Value: "desc"},
Type: &wrappers.StringValue{Value: "ThisIsMadeUp"},
}},
errCode: codes.InvalidArgument,
},
{
name: "Create with no type",
req: &pbs.CreateHostCatalogRequest{Item: &pb.HostCatalog{
Name: &wrappers.StringValue{Value: "name"},
Description: &wrappers.StringValue{Value: "desc"},
}},
errCode: codes.InvalidArgument,
},
{
name: "Can't specify Id",
req: &pbs.CreateHostCatalogRequest{Item: &pb.HostCatalog{
Id: "not allowed to be set",
}},
res: nil,
errCode: codes.InvalidArgument,
},
{
name: "Can't specify Created Time",
req: &pbs.CreateHostCatalogRequest{Item: &pb.HostCatalog{
CreatedTime: ptypes.TimestampNow(),
}},
res: nil,
errCode: codes.InvalidArgument,
},
{
name: "Can't specify Update Time",
req: &pbs.CreateHostCatalogRequest{Item: &pb.HostCatalog{
UpdatedTime: ptypes.TimestampNow(),
}},
res: nil,
errCode: codes.InvalidArgument,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := proto.Clone(toMerge).(*pbs.CreateHostCatalogRequest)
proto.Merge(req, tc.req)
s, err := host_catalogs.NewService(repo)
require.NoError(err, "Failed to create a new host catalog service.")
got, gErr := s.CreateHostCatalog(context.Background(), req)
assert.Equal(tc.errCode, status.Code(gErr), "CreateHostCatalog(%+v) got error %v, wanted %v", req, gErr, tc.errCode)
if got != nil {
assert.True(strings.HasPrefix(got.GetUri(), tc.res.GetUri()))
assert.True(strings.HasPrefix(got.GetItem().GetId(), static.HostCatalogPrefix))
gotCreateTime, err := ptypes.Timestamp(got.GetItem().GetCreatedTime())
require.NoError(err, "Error converting proto to timestamp.")
gotUpdateTime, err := ptypes.Timestamp(got.GetItem().GetUpdatedTime())
require.NoError(err, "Error converting proto to timestamp")
// Verify it is a catalog created after the test setup's default catalog
assert.True(gotCreateTime.After(defaultHcCreated), "New catalog should have been created after default catalog. Was created %v, which is after %v", gotCreateTime, defaultHcCreated)
assert.True(gotUpdateTime.After(defaultHcCreated), "New catalog should have been updated after default catalog. Was updated %v, which is after %v", gotUpdateTime, defaultHcCreated)
// Clear all values which are hard to compare against.
got.Uri, tc.res.Uri = "", ""
got.Item.Id, tc.res.Item.Id = "", ""
got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil
}
assert.True(proto.Equal(got, tc.res), "CreateHostCatalog(%q) got response %q, wanted %q", req, got, tc.res)
})
}
}
func TestUpdate(t *testing.T) {
t.Parallel()
require := require.New(t)
assert := assert.New(t)
hc, proj, repoFn := createDefaultHostCatalogAndRepo(t)
tested, err := host_catalogs.NewService(repoFn)
require.NoError(err, "Failed to create a new host catalog service.")
resetHostCatalog := func() {
repo, err := repoFn()
require.NoError(err, "Couldn't create new static repo.")
hc, _, err = repo.UpdateCatalog(context.Background(), hc, []string{"Name", "Description"})
require.NoError(err, "Failed to reset host catalog.")
}
hcCreated, err := ptypes.Timestamp(hc.GetCreateTime().GetTimestamp())
require.NoError(err, "Failed to convert proto to timestamp")
toMerge := &pbs.UpdateHostCatalogRequest{
OrgId: proj.GetParentId(),
ProjectId: proj.GetPublicId(),
Id: hc.GetPublicId(),
}
cases := []struct {
name string
req *pbs.UpdateHostCatalogRequest
res *pbs.UpdateHostCatalogResponse
errCode codes.Code
}{
{
name: "Update an Existing HostCatalog",
req: &pbs.UpdateHostCatalogRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"name", "description"},
},
Item: &pb.HostCatalog{
Name: &wrappers.StringValue{Value: "new"},
Description: &wrappers.StringValue{Value: "desc"},
},
},
res: &pbs.UpdateHostCatalogResponse{
Item: &pb.HostCatalog{
Id: hc.GetPublicId(),
Name: &wrappers.StringValue{Value: "new"},
Description: &wrappers.StringValue{Value: "desc"},
CreatedTime: hc.GetCreateTime().GetTimestamp(),
Type: &wrappers.StringValue{Value: "Static"},
},
},
errCode: codes.OK,
},
{
name: "Multiple Paths in single string",
req: &pbs.UpdateHostCatalogRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"name,description"},
},
Item: &pb.HostCatalog{
Name: &wrappers.StringValue{Value: "new"},
Description: &wrappers.StringValue{Value: "desc"},
},
},
res: &pbs.UpdateHostCatalogResponse{
Item: &pb.HostCatalog{
Id: hc.GetPublicId(),
Name: &wrappers.StringValue{Value: "new"},
Description: &wrappers.StringValue{Value: "desc"},
CreatedTime: hc.GetCreateTime().GetTimestamp(),
Type: &wrappers.StringValue{Value: "Static"},
},
},
errCode: codes.OK,
},
{
name: "No Update Mask",
req: &pbs.UpdateHostCatalogRequest{
Item: &pb.HostCatalog{
Name: &wrappers.StringValue{Value: "updated name"},
Description: &wrappers.StringValue{Value: "updated desc"},
},
},
errCode: codes.InvalidArgument,
},
{
name: "Empty Path",
req: &pbs.UpdateHostCatalogRequest{
UpdateMask: &field_mask.FieldMask{Paths: []string{}},
Item: &pb.HostCatalog{
Name: &wrappers.StringValue{Value: "updated name"},
Description: &wrappers.StringValue{Value: "updated desc"},
},
},
errCode: codes.InvalidArgument,
},
{
name: "Only non-existant paths in Mask",
req: &pbs.UpdateHostCatalogRequest{
UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistant_field"}},
Item: &pb.HostCatalog{
Name: &wrappers.StringValue{Value: "updated name"},
Description: &wrappers.StringValue{Value: "updated desc"},
},
},
errCode: codes.InvalidArgument,
},
{
name: "Unset Name",
req: &pbs.UpdateHostCatalogRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"name"},
},
Item: &pb.HostCatalog{
Description: &wrappers.StringValue{Value: "ignored"},
},
},
res: &pbs.UpdateHostCatalogResponse{
Item: &pb.HostCatalog{
Id: hc.GetPublicId(),
Description: &wrappers.StringValue{Value: "default"},
CreatedTime: hc.GetCreateTime().GetTimestamp(),
Type: &wrappers.StringValue{Value: "Static"},
},
},
errCode: codes.OK,
},
{
name: "Unset Description",
req: &pbs.UpdateHostCatalogRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"description"},
},
Item: &pb.HostCatalog{
Name: &wrappers.StringValue{Value: "ignored"},
},
},
res: &pbs.UpdateHostCatalogResponse{
Item: &pb.HostCatalog{
Id: hc.GetPublicId(),
Name: &wrappers.StringValue{Value: "default"},
CreatedTime: hc.GetCreateTime().GetTimestamp(),
Type: &wrappers.StringValue{Value: "Static"},
},
},
errCode: codes.OK,
},
{
name: "Update Only Name",
req: &pbs.UpdateHostCatalogRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"name"},
},
Item: &pb.HostCatalog{
Name: &wrappers.StringValue{Value: "updated"},
Description: &wrappers.StringValue{Value: "ignored"},
},
},
res: &pbs.UpdateHostCatalogResponse{
Item: &pb.HostCatalog{
Id: hc.GetPublicId(),
Name: &wrappers.StringValue{Value: "updated"},
Description: &wrappers.StringValue{Value: "default"},
CreatedTime: hc.GetCreateTime().GetTimestamp(),
Type: &wrappers.StringValue{Value: "Static"},
},
},
errCode: codes.OK,
},
{
name: "Update Only Description",
req: &pbs.UpdateHostCatalogRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"description"},
},
Item: &pb.HostCatalog{
Name: &wrappers.StringValue{Value: "ignored"},
Description: &wrappers.StringValue{Value: "notignored"},
},
},
res: &pbs.UpdateHostCatalogResponse{
Item: &pb.HostCatalog{
Id: hc.GetPublicId(),
Name: &wrappers.StringValue{Value: "default"},
Description: &wrappers.StringValue{Value: "notignored"},
CreatedTime: hc.GetCreateTime().GetTimestamp(),
Type: &wrappers.StringValue{Value: "Static"},
},
},
errCode: codes.OK,
},
// TODO: Updating a non existing catalog should result in a NotFound exception but currently results in
// the repoFn returning an internal error.
{
name: "Update a Non Existing HostCatalog",
req: &pbs.UpdateHostCatalogRequest{
Id: static.HostCatalogPrefix + "_DoesntExis",
UpdateMask: &field_mask.FieldMask{
Paths: []string{"description"},
},
Item: &pb.HostCatalog{
Name: &wrappers.StringValue{Value: "new"},
Description: &wrappers.StringValue{Value: "desc"},
},
},
errCode: codes.Internal,
},
{
name: "Cant change Id",
req: &pbs.UpdateHostCatalogRequest{
Id: hc.GetPublicId(),
UpdateMask: &field_mask.FieldMask{
Paths: []string{"id"},
},
Item: &pb.HostCatalog{
Id: "p_somethinge",
Name: &wrappers.StringValue{Value: "new"},
Description: &wrappers.StringValue{Value: "new desc"},
}},
res: nil,
errCode: codes.InvalidArgument,
},
{
name: "Cant specify Created Time",
req: &pbs.UpdateHostCatalogRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"created_time"},
},
Item: &pb.HostCatalog{
CreatedTime: ptypes.TimestampNow(),
},
},
res: nil,
errCode: codes.InvalidArgument,
},
{
name: "Cant specify Updated Time",
req: &pbs.UpdateHostCatalogRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"updated_time"},
},
Item: &pb.HostCatalog{
UpdatedTime: ptypes.TimestampNow(),
},
},
res: nil,
errCode: codes.InvalidArgument,
},
{
name: "Valid mask, cant specify type",
req: &pbs.UpdateHostCatalogRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"name"},
},
Item: &pb.HostCatalog{
Type: &wrappers.StringValue{Value: "Unknown"},
},
},
res: nil,
errCode: codes.InvalidArgument,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
defer resetHostCatalog()
req := proto.Clone(toMerge).(*pbs.UpdateHostCatalogRequest)
proto.Merge(req, tc.req)
got, gErr := tested.UpdateHostCatalog(context.Background(), req)
assert.Equal(tc.errCode, status.Code(gErr), "UpdateHostCatalog(%+v) got error %v, wanted %v", req, gErr, tc.errCode)
if got != nil {
assert.NotNilf(tc.res, "Expected UpdateHostCatalog response to be nil, but was %v", got)
gotUpdateTime, err := ptypes.Timestamp(got.GetItem().GetUpdatedTime())
require.NoError(err, "Failed to convert proto to timestamp")
// Verify it is a catalog updated after it was created
// TODO: This is currently failing.
//assert.True(gotUpdateTime.After(hcCreated), "Updated catalog should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, hcCreated)
_ = gotUpdateTime
_ = hcCreated
// Clear all values which are hard to compare against.
got.Item.UpdatedTime, tc.res.Item.UpdatedTime = nil, nil
}
assert.True(proto.Equal(got, tc.res), "UpdateHostCatalog(%q) got response %q, wanted %q", req, got, tc.res)
})
}
}
@@ -1,23 +0,0 @@
package host_catalogs
import (
"context"
"github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type Service struct {
*services.UnimplementedHostCatalogServiceServer
}
func (s Service) ListHostCatalogs(ctx context.Context, req *services.ListHostCatalogsRequest) (*services.ListHostCatalogsResponse, error) {
return nil, status.Errorf(codes.NotFound, "Org %q not found", req.OrgId)
}
func (s Service) DeleteHostCatalog(ctx context.Context, req *services.DeleteHostCatalogRequest) (*services.DeleteHostCatalogResponse, error) {
return &services.DeleteHostCatalogResponse{Existed: false}, nil
}
var _ services.HostCatalogServiceServer = &Service{}
@@ -1,48 +0,0 @@
package host_catalogs_test
import (
"context"
"testing"
"github.com/golang/protobuf/proto"
pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/host_catalogs"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func TestDelete(t *testing.T) {
toMerge := &pbs.DeleteHostCatalogRequest{
OrgId: "1",
ProjectId: "2",
Id: "3",
}
s := host_catalogs.Service{}
cases := []struct {
name string
req *pbs.DeleteHostCatalogRequest
res *pbs.DeleteHostCatalogResponse
errCode codes.Code
}{
{
name: "Delete always succeeds even for non existant catalogs",
req: &pbs.DeleteHostCatalogRequest{Id: "This doesn't exist."},
res: &pbs.DeleteHostCatalogResponse{},
errCode: codes.OK,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := proto.Clone(toMerge).(*pbs.DeleteHostCatalogRequest)
proto.Merge(req, tc.req)
got, gErr := s.DeleteHostCatalog(context.Background(), req)
if status.Code(gErr) != tc.errCode {
t.Errorf("DeleteHostCatalog(%+v) got error %v, wanted %v", req, gErr, tc.errCode)
}
if !proto.Equal(got, tc.res) {
t.Errorf("DeleteHostCatalog(%q) got response %q, wanted %q", req, got, tc.res)
}
})
}
}
@@ -7,7 +7,6 @@ import (
"strings"
"github.com/golang/protobuf/ptypes/wrappers"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
pb "github.com/hashicorp/watchtower/internal/gen/controller/api/resources/scopes"
pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/iam"
@@ -27,14 +26,14 @@ var (
type Service struct {
pbs.UnimplementedProjectServiceServer
repo *iam.Repository
repoFn func() (*iam.Repository, error)
}
func NewService(repo *iam.Repository) *Service {
if repo == nil {
func NewService(repoFn func() (*iam.Repository, error)) *Service {
if repoFn == nil {
return nil
}
return &Service{repo: repo}
return &Service{repoFn: repoFn}
}
var _ pbs.ProjectServiceServer = &Service{}
@@ -93,7 +92,11 @@ func (s Service) DeleteProject(ctx context.Context, req *pbs.DeleteProjectReques
}
func (s Service) getFromRepo(ctx context.Context, id string) (*pb.Project, error) {
p, err := s.repo.LookupScope(ctx, id)
repo, err := s.repoFn()
if err != nil {
return nil, err
}
p, err := repo.LookupScope(ctx, id)
if err != nil {
return nil, err
}
@@ -115,7 +118,11 @@ func (s Service) createInRepo(ctx context.Context, orgID string, item *pb.Projec
if err != nil {
return nil, status.Errorf(codes.Internal, "Unable to build project for creation: %v.", err)
}
out, err := s.repo.CreateScope(ctx, p)
repo, err := s.repoFn()
if err != nil {
return nil, err
}
out, err := repo.CreateScope(ctx, p)
if err != nil {
return nil, status.Errorf(codes.Internal, "Unable to create project: %v.", err)
}
@@ -144,7 +151,11 @@ func (s Service) updateInRepo(ctx context.Context, orgID, projId string, mask []
if len(dbMask) == 0 {
return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", []string{"update_mask"})
}
out, rowsUpdated, err := s.repo.UpdateScope(ctx, p, dbMask)
repo, err := s.repoFn()
if err != nil {
return nil, err
}
out, rowsUpdated, err := repo.UpdateScope(ctx, p, dbMask)
if err != nil {
return nil, status.Errorf(codes.Internal, "Unable to update project: %v.", err)
}
@@ -155,7 +166,11 @@ func (s Service) updateInRepo(ctx context.Context, orgID, projId string, mask []
}
func (s Service) deleteFromRepo(ctx context.Context, projId string) (bool, error) {
rows, err := s.repo.DeleteScope(ctx, projId)
repo, err := s.repoFn()
if err != nil {
return false, err
}
rows, err := repo.DeleteScope(ctx, projId)
if err != nil {
return false, status.Errorf(codes.Internal, "Unable to delete project: %v.", err)
}
@@ -328,8 +343,3 @@ func validateAncestors(r ancestorProvider) error {
}
return nil
}
// RegisterGrpcGateway satisfies the RegisterGrpcGatewayer interface.
func (s *Service) RegisterGrpcGateway(mux *runtime.ServeMux) error {
return pbs.RegisterProjectServiceHandlerServer(context.Background(), mux, s)
}
@@ -14,6 +14,7 @@ import (
pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/iam"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/projects"
"github.com/stretchr/testify/require"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -21,7 +22,7 @@ import (
"github.com/stretchr/testify/assert"
)
func createDefaultProjectAndRepo(t *testing.T) (*iam.Scope, *iam.Repository) {
func createDefaultProjectAndRepo(t *testing.T) (*iam.Scope, func() (*iam.Repository, error)) {
t.Helper()
cleanup, conn, _ := db.TestSetup(t, "postgres")
t.Cleanup(func() {
@@ -30,8 +31,11 @@ func createDefaultProjectAndRepo(t *testing.T) (*iam.Scope, *iam.Repository) {
})
rw := db.New(conn)
wrap := db.TestWrapper(t)
repo, err := iam.NewRepository(rw, rw, wrap)
assert.Nil(t, err, "Unable to create new repo")
repoFn := func() (*iam.Repository, error) {
return iam.NewRepository(rw, rw, wrap)
}
repo, err := repoFn()
assert.Nil(t, err, "Unable to create new repoFn")
// Create a default org and project for our tests.
o, err := iam.NewOrganization(iam.WithName("default"))
@@ -52,7 +56,7 @@ func createDefaultProjectAndRepo(t *testing.T) (*iam.Scope, *iam.Repository) {
t.Fatalf("Could not create project scope: %v", err)
}
return pRes, repo
return pRes, repoFn
}
func TestGet(t *testing.T) {
@@ -120,18 +124,20 @@ func TestGet(t *testing.T) {
}
func TestDelete(t *testing.T) {
proj, repo := createDefaultProjectAndRepo(t)
proj, repoFn := createDefaultProjectAndRepo(t)
proj2, err := iam.NewProject(proj.GetParentId())
if err != nil {
t.Fatalf("Couldn't allocate a second project: %v", err)
}
repo, err := repoFn()
require.NoError(t, err, "Error creating new iam repo.")
proj2, err = repo.CreateScope(context.Background(), proj2)
if err != nil {
t.Fatalf("Couldn't persist a second project %v", err)
}
s := projects.NewService(repo)
s := projects.NewService(repoFn)
cases := []struct {
name string
@@ -285,8 +291,8 @@ func TestCreate(t *testing.T) {
got, gErr := s.CreateProject(context.Background(), req)
assert.Equal(tc.errCode, status.Code(gErr), "CreateProject(%+v) got error %v, wanted %v", req, gErr, tc.errCode)
if got != nil {
strings.HasPrefix(got.GetUri(), tc.res.Uri)
strings.HasPrefix(got.GetItem().GetId(), "p_")
assert.True(strings.HasPrefix(got.GetUri(), tc.res.Uri))
assert.True(strings.HasPrefix(got.GetItem().GetId(), "p_"))
gotCreateTime, err := ptypes.Timestamp(got.GetItem().GetCreatedTime())
if err != nil {
t.Fatalf("Error converting proto to timestamp: %v", err)
@@ -310,11 +316,13 @@ func TestCreate(t *testing.T) {
}
func TestUpdate(t *testing.T) {
proj, repo := createDefaultProjectAndRepo(t)
tested := projects.NewService(repo)
proj, repoFn := createDefaultProjectAndRepo(t)
tested := projects.NewService(repoFn)
var err error
resetProject := func() {
repo, err := repoFn()
require.NoError(t, err, "Error creating new iam repo.")
if proj, _, err = repo.UpdateScope(context.Background(), proj, []string{"Name", "Description"}); err != nil {
t.Fatalf("Failed to reset the project")
}
@@ -378,7 +386,7 @@ func TestUpdate(t *testing.T) {
errCode: codes.OK,
},
{
name: "No Update Mask Is Invalid Argument",
name: "No Update Mask",
req: &pbs.UpdateProjectRequest{
Item: &pb.Project{
Name: &wrappers.StringValue{Value: "updated name"},
@@ -388,7 +396,7 @@ func TestUpdate(t *testing.T) {
errCode: codes.InvalidArgument,
},
{
name: "No Paths in Mask Is Invalid Argument",
name: "No Paths in Mask",
req: &pbs.UpdateProjectRequest{
UpdateMask: &field_mask.FieldMask{Paths: []string{}},
Item: &pb.Project{
@@ -399,7 +407,7 @@ func TestUpdate(t *testing.T) {
errCode: codes.InvalidArgument,
},
{
name: "Only non-existant paths in Mask Is Invalid Argument",
name: "Only non-existant paths in Mask",
req: &pbs.UpdateProjectRequest{
UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistant_field"}},
Item: &pb.Project{
@@ -428,6 +436,25 @@ func TestUpdate(t *testing.T) {
},
errCode: codes.OK,
},
{
name: "Unset Description",
req: &pbs.UpdateProjectRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"description"},
},
Item: &pb.Project{
Name: &wrappers.StringValue{Value: "ignored"},
},
},
res: &pbs.UpdateProjectResponse{
Item: &pb.Project{
Id: proj.GetPublicId(),
Name: &wrappers.StringValue{Value: "default"},
CreatedTime: proj.GetCreateTime().GetTimestamp(),
},
},
errCode: codes.OK,
},
{
name: "Update Only Name",
req: &pbs.UpdateProjectRequest{
@@ -470,8 +497,8 @@ func TestUpdate(t *testing.T) {
},
errCode: codes.OK,
},
// TODO: Updating a non existant project should result in a NotFound exception but currently results in
// the repo returning an internal error.
// TODO: Updating a non existing project should result in a NotFound exception but currently results in
// the repoFn returning an internal error.
{
name: "Update a Non Existing Project",
req: &pbs.UpdateProjectRequest{
+4 -1
View File
@@ -21,9 +21,12 @@ func (c *Controller) startListeners() error {
servers := make([]func(), 0, len(c.conf.Listeners))
configureForAPI := func(ln *base.ServerListener) error {
handler := c.handler(HandlerProperties{
handler, err := c.handler(HandlerProperties{
ListenerConfig: ln.Config,
})
if err != nil {
return err
}
/*
// TODO: As I write this Vault's having this code audited, make sure to