docker/swarmkit

View on GitHub
manager/controlapi/resource.go

Summary

Maintainability
A
3 hrs
Test Coverage
package controlapi

import (
    "context"

    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"

    "github.com/moby/swarmkit/v2/api"
    "github.com/moby/swarmkit/v2/identity"
    "github.com/moby/swarmkit/v2/log"
    "github.com/moby/swarmkit/v2/manager/state/store"
)

// CreateResource returns a `CreateResourceResponse` after creating a `Resource` based
// on the provided `CreateResourceRequest.Resource`.
//   - Returns `InvalidArgument` if the `CreateResourceRequest.Resource` is malformed,
//     or if the config data is too long or contains invalid characters.
//   - Returns an error if the creation fails.
func (s *Server) CreateResource(ctx context.Context, request *api.CreateResourceRequest) (*api.CreateResourceResponse, error) {
    if request.Annotations == nil || request.Annotations.Name == "" {
        return nil, status.Errorf(codes.InvalidArgument, "Resource must have a name")
    }

    // finally, validate that Kind is not an emptystring. We know that creating
    // with Kind as empty string should fail at the store level, but to make
    // errors clearer, special case this.
    if request.Kind == "" {
        return nil, status.Errorf(codes.InvalidArgument, "Resource must belong to an Extension")
    }
    r := &api.Resource{
        ID:          identity.NewID(),
        Annotations: *request.Annotations,
        Kind:        request.Kind,
        Payload:     request.Payload,
    }

    err := s.store.Update(func(tx store.Tx) error {
        return store.CreateResource(tx, r)
    })

    switch err {
    case store.ErrNoKind:
        return nil, status.Errorf(codes.InvalidArgument, "Kind %v is not registered", r.Kind)
    case store.ErrNameConflict:
        return nil, status.Errorf(
            codes.AlreadyExists,
            "A resource with name %v already exists",
            r.Annotations.Name,
        )
    case nil:
        log.G(ctx).WithFields(log.Fields{
            "resource.Name": r.Annotations.Name,
            "method":        "CreateResource",
        }).Debugf("resource created")
        return &api.CreateResourceResponse{Resource: r}, nil
    default:
        return nil, err
    }
}

// GetResource returns a `GetResourceResponse` with a `Resource` with the same
// id as `GetResourceRequest.Resource`
// - Returns `NotFound` if the Resource with the given id is not found.
// - Returns `InvalidArgument` if the `GetResourceRequest.Resource` is empty.
// - Returns an error if getting fails.
func (s *Server) GetResource(ctx context.Context, request *api.GetResourceRequest) (*api.GetResourceResponse, error) {
    if request.ResourceID == "" {
        return nil, status.Errorf(codes.InvalidArgument, "resource ID must be present")
    }
    var resource *api.Resource
    s.store.View(func(tx store.ReadTx) {
        resource = store.GetResource(tx, request.ResourceID)
    })

    if resource == nil {
        return nil, status.Errorf(codes.NotFound, "resource %s not found", request.ResourceID)
    }

    return &api.GetResourceResponse{Resource: resource}, nil
}

// RemoveResource removes the `Resource` referenced by `RemoveResourceRequest.ResourceID`.
// - Returns `InvalidArgument` if `RemoveResourceRequest.ResourceID` is empty.
// - Returns `NotFound` if the a resource named `RemoveResourceRequest.ResourceID` is not found.
// - Returns an error if the deletion fails.
func (s *Server) RemoveResource(ctx context.Context, request *api.RemoveResourceRequest) (*api.RemoveResourceResponse, error) {
    if request.ResourceID == "" {
        return nil, status.Errorf(codes.InvalidArgument, "resource ID must be present")
    }
    err := s.store.Update(func(tx store.Tx) error {
        return store.DeleteResource(tx, request.ResourceID)
    })
    switch err {
    case store.ErrNotExist:
        return nil, status.Errorf(codes.NotFound, "resource %s not found", request.ResourceID)
    case nil:
        return &api.RemoveResourceResponse{}, nil
    default:
        return nil, err
    }
}

// ListResources returns a `ListResourcesResponse` with a list of `Resource`s stored in the raft store,
// or all resources matching any name in `ListConfigsRequest.Names`, any
// name prefix in `ListResourcesRequest.NamePrefixes`, any id in
// `ListResourcesRequest.ResourceIDs`, or any id prefix in `ListResourcesRequest.IDPrefixes`.
// - Returns an error if listing fails.
func (s *Server) ListResources(ctx context.Context, request *api.ListResourcesRequest) (*api.ListResourcesResponse, error) {
    var (
        resources     []*api.Resource
        respResources []*api.Resource
        err           error
        byFilters     []store.By
        by            store.By
        labels        map[string]string
    )

    // andKind is set to true if the Extension filter is not the only filter
    // being used. If this is the case, we do not have to compare by strings,
    // which could be slow.
    var andKind bool

    if request.Filters != nil {
        for _, name := range request.Filters.Names {
            byFilters = append(byFilters, store.ByName(name))
        }
        for _, prefix := range request.Filters.NamePrefixes {
            byFilters = append(byFilters, store.ByNamePrefix(prefix))
        }
        for _, prefix := range request.Filters.IDPrefixes {
            byFilters = append(byFilters, store.ByIDPrefix(prefix))
        }
        labels = request.Filters.Labels
        if request.Filters.Kind != "" {
            // if we're filtering on Extensions, then set this to true. If Kind is
            // the _only_ kind of filter, we'll set this to false below.
            andKind = true
        }
    }

    switch len(byFilters) {
    case 0:
        // NOTE(dperny): currently, filtering using store.ByKind would apply an
        // Or operation, which means that filtering by kind would return a
        // union. However, for Kind filters, we actually want the
        // _intersection_; that is, _only_ objects of the specified kind. we
        // could dig into the db code to figure out how to write and use an
        // efficient And combinator, but I don't have the time nor expertise to
        // do so at the moment. instead, we'll filter by kind after the fact.
        // however, if there are no other kinds of filters, and we're ONLY
        // listing by Kind, we can set that to be the only filter.
        if andKind {
            by = store.ByKind(request.Filters.Kind)
            andKind = false
        } else {
            by = store.All
        }
    case 1:
        by = byFilters[0]
    default:
        by = store.Or(byFilters...)
    }

    s.store.View(func(tx store.ReadTx) {
        resources, err = store.FindResources(tx, by)
    })
    if err != nil {
        return nil, err
    }

    // filter by label and extension
    for _, resource := range resources {
        if !filterMatchLabels(resource.Annotations.Labels, labels) {
            continue
        }
        if andKind && resource.Kind != request.Filters.Kind {
            continue
        }
        respResources = append(respResources, resource)
    }

    return &api.ListResourcesResponse{Resources: respResources}, nil
}

// UpdateResource updates the resource with the given `UpdateResourceRequest.Resource.Id` using the given `UpdateResourceRequest.Resource` and returns a `UpdateResourceResponse`.
// - Returns `NotFound` if the Resource with the given `UpdateResourceRequest.Resource.Id` is not found.
// - Returns `InvalidArgument` if the UpdateResourceRequest.Resource.Id` is empty.
// - Returns an error if updating fails.
func (s *Server) UpdateResource(ctx context.Context, request *api.UpdateResourceRequest) (*api.UpdateResourceResponse, error) {
    if request.ResourceID == "" || request.ResourceVersion == nil {
        return nil, status.Errorf(codes.InvalidArgument, "must include ID and version")
    }
    var r *api.Resource
    err := s.store.Update(func(tx store.Tx) error {
        r = store.GetResource(tx, request.ResourceID)
        if r == nil {
            return status.Errorf(codes.NotFound, "resource %v not found", request.ResourceID)
        }

        if request.Annotations != nil {
            if r.Annotations.Name != request.Annotations.Name {
                return status.Errorf(codes.InvalidArgument, "cannot change resource name")
            }
            r.Annotations = *request.Annotations
        }
        r.Meta.Version = *request.ResourceVersion
        // only alter the payload if the
        if request.Payload != nil {
            r.Payload = request.Payload
        }

        return store.UpdateResource(tx, r)
    })
    switch err {
    case store.ErrSequenceConflict:
        return nil, status.Errorf(codes.InvalidArgument, "update out of sequence")
    case nil:
        return &api.UpdateResourceResponse{
            Resource: r,
        }, nil
    default:
        return nil, err
    }
}