2016-06-07 17:28:28 -04:00
package controlapi
import (
2016-06-17 22:01:18 -04:00
"errors"
2016-11-02 14:43:27 -04:00
"path/filepath"
2016-06-17 22:01:18 -04:00
"reflect"
2016-10-26 09:35:48 -04:00
"regexp"
2016-07-22 13:26:45 -04:00
"strconv"
2016-11-04 15:11:41 -04:00
"strings"
2016-06-17 22:01:18 -04:00
2016-09-13 12:28:01 -04:00
"github.com/docker/distribution/reference"
2016-06-07 17:28:28 -04:00
"github.com/docker/swarmkit/api"
"github.com/docker/swarmkit/identity"
2016-10-15 11:49:04 -04:00
"github.com/docker/swarmkit/manager/constraint"
2016-06-07 17:28:28 -04:00
"github.com/docker/swarmkit/manager/state/store"
2016-06-30 16:34:48 -04:00
"github.com/docker/swarmkit/protobuf/ptypes"
2016-11-09 17:28:06 -05:00
"github.com/docker/swarmkit/template"
2016-06-07 17:28:28 -04:00
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
2016-06-17 22:01:18 -04:00
var (
errNetworkUpdateNotSupported = errors . New ( "changing network in service is not supported" )
2016-10-26 09:35:48 -04:00
errRenameNotSupported = errors . New ( "renaming services is not supported" )
2016-06-30 16:34:48 -04:00
errModeChangeNotAllowed = errors . New ( "service mode change is not allowed" )
2016-06-17 22:01:18 -04:00
)
2016-10-26 09:35:48 -04:00
// Regexp pattern for hostname to conform RFC 1123
var hostnamePattern = regexp . MustCompile ( "^(([[:alnum:]]|[[:alnum:]][[:alnum:]\\-]*[[:alnum:]])\\.)*([[:alnum:]]|[[:alnum:]][[:alnum:]\\-]*[[:alnum:]])$" )
2016-06-07 17:28:28 -04:00
func validateResources ( r * api . Resources ) error {
if r == nil {
return nil
}
if r . NanoCPUs != 0 && r . NanoCPUs < 1e6 {
return grpc . Errorf ( codes . InvalidArgument , "invalid cpu value %g: Must be at least %g" , float64 ( r . NanoCPUs ) / 1e9 , 1e6 / 1e9 )
}
if r . MemoryBytes != 0 && r . MemoryBytes < 4 * 1024 * 1024 {
return grpc . Errorf ( codes . InvalidArgument , "invalid memory value %d: Must be at least 4MiB" , r . MemoryBytes )
}
return nil
}
func validateResourceRequirements ( r * api . ResourceRequirements ) error {
if r == nil {
return nil
}
if err := validateResources ( r . Limits ) ; err != nil {
return err
}
if err := validateResources ( r . Reservations ) ; err != nil {
return err
}
return nil
}
2016-06-30 16:34:48 -04:00
func validateRestartPolicy ( rp * api . RestartPolicy ) error {
if rp == nil {
return nil
}
if rp . Delay != nil {
delay , err := ptypes . Duration ( rp . Delay )
if err != nil {
return err
}
if delay < 0 {
return grpc . Errorf ( codes . InvalidArgument , "TaskSpec: restart-delay cannot be negative" )
}
}
if rp . Window != nil {
win , err := ptypes . Duration ( rp . Window )
if err != nil {
return err
}
if win < 0 {
return grpc . Errorf ( codes . InvalidArgument , "TaskSpec: restart-window cannot be negative" )
}
}
return nil
}
2016-07-07 07:58:43 -04:00
func validatePlacement ( placement * api . Placement ) error {
if placement == nil {
return nil
}
2016-10-15 11:49:04 -04:00
_ , err := constraint . Parse ( placement . Constraints )
2016-07-07 07:58:43 -04:00
return err
}
2016-06-30 16:34:48 -04:00
func validateUpdate ( uc * api . UpdateConfig ) error {
if uc == nil {
return nil
}
delay , err := ptypes . Duration ( & uc . Delay )
if err != nil {
return err
}
if delay < 0 {
return grpc . Errorf ( codes . InvalidArgument , "TaskSpec: update-delay cannot be negative" )
}
return nil
}
2016-10-26 09:35:48 -04:00
func validateContainerSpec ( container * api . ContainerSpec ) error {
if container == nil {
return grpc . Errorf ( codes . InvalidArgument , "ContainerSpec: missing in service spec" )
}
if err := validateHostname ( container . Hostname ) ; err != nil {
return err
}
if container . Image == "" {
return grpc . Errorf ( codes . InvalidArgument , "ContainerSpec: image reference must be provided" )
}
if _ , err := reference . ParseNamed ( container . Image ) ; err != nil {
return grpc . Errorf ( codes . InvalidArgument , "ContainerSpec: %q is not a valid repository/tag" , container . Image )
}
mountMap := make ( map [ string ] bool )
for _ , mount := range container . Mounts {
if _ , exists := mountMap [ mount . Target ] ; exists {
return grpc . Errorf ( codes . InvalidArgument , "ContainerSpec: duplicate mount point: %s" , mount . Target )
}
mountMap [ mount . Target ] = true
}
return nil
}
func validateHostname ( hostname string ) error {
if hostname != "" {
if len ( hostname ) > 63 || ! hostnamePattern . MatchString ( hostname ) {
return grpc . Errorf ( codes . InvalidArgument , "ContainerSpec: %s is not valid hostname" , hostname )
}
}
return nil
}
2016-06-30 16:34:48 -04:00
func validateTask ( taskSpec api . TaskSpec ) error {
if err := validateResourceRequirements ( taskSpec . Resources ) ; err != nil {
2016-06-07 17:28:28 -04:00
return err
}
2016-06-30 16:34:48 -04:00
if err := validateRestartPolicy ( taskSpec . Restart ) ; err != nil {
return err
}
2016-07-07 07:58:43 -04:00
if err := validatePlacement ( taskSpec . Placement ) ; err != nil {
return err
}
2016-06-30 16:34:48 -04:00
if taskSpec . GetRuntime ( ) == nil {
2016-06-07 17:28:28 -04:00
return grpc . Errorf ( codes . InvalidArgument , "TaskSpec: missing runtime" )
}
2016-06-30 16:34:48 -04:00
_ , ok := taskSpec . GetRuntime ( ) . ( * api . TaskSpec_Container )
2016-06-07 17:28:28 -04:00
if ! ok {
return grpc . Errorf ( codes . Unimplemented , "RuntimeSpec: unimplemented runtime in service spec" )
}
2016-11-09 17:28:06 -05:00
// Building a empty/dummy Task to validate the templating and
// the resulting container spec as well. This is a *best effort*
// validation.
preparedSpec , err := template . ExpandContainerSpec ( & api . Task {
Spec : taskSpec ,
ServiceID : "serviceid" ,
Slot : 1 ,
NodeID : "nodeid" ,
Networks : [ ] * api . NetworkAttachment { } ,
Annotations : api . Annotations {
Name : "taskname" ,
} ,
ServiceAnnotations : api . Annotations {
Name : "servicename" ,
} ,
Endpoint : & api . Endpoint { } ,
LogDriver : taskSpec . LogDriver ,
} )
if err != nil {
return grpc . Errorf ( codes . InvalidArgument , err . Error ( ) )
}
if err := validateContainerSpec ( preparedSpec ) ; err != nil {
2016-10-26 09:35:48 -04:00
return err
2016-10-03 13:58:05 -04:00
}
2016-06-07 17:28:28 -04:00
return nil
}
2016-06-17 22:01:18 -04:00
func validateEndpointSpec ( epSpec * api . EndpointSpec ) error {
// Endpoint spec is optional
if epSpec == nil {
return nil
}
2016-10-20 14:26:04 -04:00
type portSpec struct {
publishedPort uint32
protocol api . PortConfig_Protocol
}
portSet := make ( map [ portSpec ] struct { } )
2016-06-17 22:01:18 -04:00
for _ , port := range epSpec . Ports {
2016-11-16 21:15:15 -05:00
// Publish mode = "ingress" represents Routing-Mesh and current implementation
// of routing-mesh relies on IPVS based load-balancing with input=published-port.
// But Endpoint-Spec mode of DNSRR relies on multiple A records and cannot be used
// with routing-mesh (PublishMode="ingress") which cannot rely on DNSRR.
// But PublishMode="host" doesn't provide Routing-Mesh and the DNSRR is applicable
// for the backend network and hence we accept that configuration.
if epSpec . Mode == api . ResolutionModeDNSRoundRobin && port . PublishMode == api . PublishModeIngress {
return grpc . Errorf ( codes . InvalidArgument , "EndpointSpec: port published with ingress mode can't be used with dnsrr mode" )
}
2016-10-20 14:26:04 -04:00
// If published port is not specified, it does not conflict
// with any others.
if port . PublishedPort == 0 {
continue
}
portSpec := portSpec { publishedPort : port . PublishedPort , protocol : port . Protocol }
if _ , ok := portSet [ portSpec ] ; ok {
2016-09-13 12:28:01 -04:00
return grpc . Errorf ( codes . InvalidArgument , "EndpointSpec: duplicate published ports provided" )
2016-06-17 22:01:18 -04:00
}
2016-10-20 14:26:04 -04:00
portSet [ portSpec ] = struct { } { }
2016-06-17 22:01:18 -04:00
}
return nil
}
2016-11-04 15:11:41 -04:00
// validateSecretRefsSpec finds if the secrets passed in spec are valid and have no
// conflicting targets.
func validateSecretRefsSpec ( spec * api . ServiceSpec ) error {
container := spec . Task . GetContainer ( )
if container == nil {
return nil
}
// Keep a map to track all the targets that will be exposed
// The string returned is only used for logging. It could as well be struct{}{}
existingTargets := make ( map [ string ] string )
for _ , secretRef := range container . Secrets {
// SecretID and SecretName are mandatory, we have invalid references without them
if secretRef . SecretID == "" || secretRef . SecretName == "" {
return grpc . Errorf ( codes . InvalidArgument , "malformed secret reference" )
}
// Every secret referece requires a Target
if secretRef . GetTarget ( ) == nil {
return grpc . Errorf ( codes . InvalidArgument , "malformed secret reference, no target provided" )
}
// If this is a file target, we will ensure filename uniqueness
if secretRef . GetFile ( ) != nil {
fileName := secretRef . GetFile ( ) . Name
// Validate the file name
if fileName == "" || fileName != filepath . Base ( filepath . Clean ( fileName ) ) {
return grpc . Errorf ( codes . InvalidArgument , "malformed file secret reference, invalid target file name provided" )
}
// If this target is already in use, we have conflicting targets
if prevSecretName , ok := existingTargets [ fileName ] ; ok {
return grpc . Errorf ( codes . InvalidArgument , "secret references '%s' and '%s' have a conflicting target: '%s'" , prevSecretName , secretRef . SecretName , fileName )
}
existingTargets [ fileName ] = secretRef . SecretName
}
}
return nil
}
2016-10-15 11:49:04 -04:00
func ( s * Server ) validateNetworks ( networks [ ] * api . NetworkAttachmentConfig ) error {
for _ , na := range networks {
var network * api . Network
s . store . View ( func ( tx store . ReadTx ) {
network = store . GetNetwork ( tx , na . Target )
} )
if network == nil {
continue
}
if _ , ok := network . Spec . Annotations . Labels [ "com.docker.swarm.internal" ] ; ok {
return grpc . Errorf ( codes . InvalidArgument ,
"Service cannot be explicitly attached to %q network which is a swarm internal network" ,
network . Spec . Annotations . Name )
}
}
return nil
}
2016-06-07 17:28:28 -04:00
func validateServiceSpec ( spec * api . ServiceSpec ) error {
if spec == nil {
return grpc . Errorf ( codes . InvalidArgument , errInvalidArgument . Error ( ) )
}
if err := validateAnnotations ( spec . Annotations ) ; err != nil {
return err
}
2016-06-30 16:34:48 -04:00
if err := validateTask ( spec . Task ) ; err != nil {
return err
}
if err := validateUpdate ( spec . Update ) ; err != nil {
return err
}
if err := validateEndpointSpec ( spec . Endpoint ) ; err != nil {
2016-06-07 17:28:28 -04:00
return err
}
2016-11-04 15:11:41 -04:00
// Check to see if the Secret Reference portion of the spec is valid
if err := validateSecretRefsSpec ( spec ) ; err != nil {
return err
}
2016-06-07 17:28:28 -04:00
return nil
}
2016-07-22 13:26:45 -04:00
// checkPortConflicts does a best effort to find if the passed in spec has port
// conflicts with existing services.
2016-08-11 19:41:44 -04:00
// `serviceID string` is the service ID of the spec in service update. If
// `serviceID` is not "", then conflicts check will be skipped against this
// service (the service being updated).
2016-08-09 14:49:39 -04:00
func ( s * Server ) checkPortConflicts ( spec * api . ServiceSpec , serviceID string ) error {
2016-07-22 13:26:45 -04:00
if spec . Endpoint == nil {
return nil
}
pcToString := func ( pc * api . PortConfig ) string {
port := strconv . FormatUint ( uint64 ( pc . PublishedPort ) , 10 )
return port + "/" + pc . Protocol . String ( )
}
reqPorts := make ( map [ string ] bool )
for _ , pc := range spec . Endpoint . Ports {
if pc . PublishedPort > 0 {
reqPorts [ pcToString ( pc ) ] = true
}
}
if len ( reqPorts ) == 0 {
return nil
}
var (
services [ ] * api . Service
err error
)
s . store . View ( func ( tx store . ReadTx ) {
services , err = store . FindServices ( tx , store . All )
} )
if err != nil {
return err
}
for _ , service := range services {
2016-08-09 14:49:39 -04:00
// If service ID is the same (and not "") then this is an update
if serviceID != "" && serviceID == service . ID {
continue
}
2016-07-22 13:26:45 -04:00
if service . Spec . Endpoint != nil {
for _ , pc := range service . Spec . Endpoint . Ports {
if reqPorts [ pcToString ( pc ) ] {
2016-08-09 14:49:39 -04:00
return grpc . Errorf ( codes . InvalidArgument , "port '%d' is already in use by service '%s' (%s)" , pc . PublishedPort , service . Spec . Annotations . Name , service . ID )
2016-07-22 13:26:45 -04:00
}
}
}
if service . Endpoint != nil {
for _ , pc := range service . Endpoint . Ports {
if reqPorts [ pcToString ( pc ) ] {
2016-08-09 14:49:39 -04:00
return grpc . Errorf ( codes . InvalidArgument , "port '%d' is already in use by service '%s' (%s)" , pc . PublishedPort , service . Spec . Annotations . Name , service . ID )
2016-07-22 13:26:45 -04:00
}
}
}
}
return nil
}
2016-11-04 15:11:41 -04:00
// checkSecretExistence finds if the secret exists
func ( s * Server ) checkSecretExistence ( tx store . Tx , spec * api . ServiceSpec ) error {
2016-10-26 09:35:48 -04:00
container := spec . Task . GetContainer ( )
if container == nil {
return nil
}
2016-11-04 15:11:41 -04:00
var failedSecrets [ ] string
2016-10-26 09:35:48 -04:00
for _ , secretRef := range container . Secrets {
2016-11-04 15:11:41 -04:00
secret := store . GetSecret ( tx , secretRef . SecretID )
// Check to see if the secret exists and secretRef.SecretName matches the actual secretName
if secret == nil || secret . Spec . Annotations . Name != secretRef . SecretName {
failedSecrets = append ( failedSecrets , secretRef . SecretName )
2016-10-26 09:35:48 -04:00
}
2016-11-04 15:11:41 -04:00
}
2016-10-26 09:35:48 -04:00
2016-11-04 15:11:41 -04:00
if len ( failedSecrets ) > 0 {
secretStr := "secrets"
if len ( failedSecrets ) == 1 {
secretStr = "secret"
2016-11-02 14:43:27 -04:00
}
2016-11-04 15:11:41 -04:00
return grpc . Errorf ( codes . InvalidArgument , "%s not found: %v" , secretStr , strings . Join ( failedSecrets , ", " ) )
2016-11-02 14:43:27 -04:00
2016-10-26 09:35:48 -04:00
}
return nil
}
2016-06-07 17:28:28 -04:00
// CreateService creates and return a Service based on the provided ServiceSpec.
// - Returns `InvalidArgument` if the ServiceSpec is malformed.
// - Returns `Unimplemented` if the ServiceSpec references unimplemented features.
// - Returns `AlreadyExists` if the ServiceID conflicts.
// - Returns an error if the creation fails.
func ( s * Server ) CreateService ( ctx context . Context , request * api . CreateServiceRequest ) ( * api . CreateServiceResponse , error ) {
if err := validateServiceSpec ( request . Spec ) ; err != nil {
return nil , err
}
2016-10-15 11:49:04 -04:00
if err := s . validateNetworks ( request . Spec . Networks ) ; err != nil {
return nil , err
}
2016-08-09 14:49:39 -04:00
if err := s . checkPortConflicts ( request . Spec , "" ) ; err != nil {
2016-07-22 13:26:45 -04:00
return nil , err
}
2016-06-07 17:28:28 -04:00
// TODO(aluzzardi): Consider using `Name` as a primary key to handle
// duplicate creations. See #65
service := & api . Service {
ID : identity . NewID ( ) ,
Spec : * request . Spec ,
}
err := s . store . Update ( func ( tx store . Tx ) error {
2016-11-04 15:11:41 -04:00
// Check to see if all the secrets being added exist as objects
// in our datastore
err := s . checkSecretExistence ( tx , request . Spec )
if err != nil {
return err
}
2016-06-07 17:28:28 -04:00
return store . CreateService ( tx , service )
} )
if err != nil {
return nil , err
}
return & api . CreateServiceResponse {
Service : service ,
} , nil
}
// GetService returns a Service given a ServiceID.
// - Returns `InvalidArgument` if ServiceID is not provided.
// - Returns `NotFound` if the Service is not found.
func ( s * Server ) GetService ( ctx context . Context , request * api . GetServiceRequest ) ( * api . GetServiceResponse , error ) {
if request . ServiceID == "" {
return nil , grpc . Errorf ( codes . InvalidArgument , errInvalidArgument . Error ( ) )
}
var service * api . Service
s . store . View ( func ( tx store . ReadTx ) {
service = store . GetService ( tx , request . ServiceID )
} )
if service == nil {
return nil , grpc . Errorf ( codes . NotFound , "service %s not found" , request . ServiceID )
}
return & api . GetServiceResponse {
Service : service ,
} , nil
}
// UpdateService updates a Service referenced by ServiceID with the given ServiceSpec.
// - Returns `NotFound` if the Service is not found.
// - Returns `InvalidArgument` if the ServiceSpec is malformed.
// - Returns `Unimplemented` if the ServiceSpec references unimplemented features.
// - Returns an error if the update fails.
func ( s * Server ) UpdateService ( ctx context . Context , request * api . UpdateServiceRequest ) ( * api . UpdateServiceResponse , error ) {
if request . ServiceID == "" || request . ServiceVersion == nil {
return nil , grpc . Errorf ( codes . InvalidArgument , errInvalidArgument . Error ( ) )
}
if err := validateServiceSpec ( request . Spec ) ; err != nil {
return nil , err
}
var service * api . Service
2016-07-22 13:26:45 -04:00
s . store . View ( func ( tx store . ReadTx ) {
service = store . GetService ( tx , request . ServiceID )
} )
if service == nil {
return nil , grpc . Errorf ( codes . NotFound , "service %s not found" , request . ServiceID )
}
if request . Spec . Endpoint != nil && ! reflect . DeepEqual ( request . Spec . Endpoint , service . Spec . Endpoint ) {
2016-08-09 14:49:39 -04:00
if err := s . checkPortConflicts ( request . Spec , request . ServiceID ) ; err != nil {
2016-07-22 13:26:45 -04:00
return nil , err
}
}
2016-06-07 17:28:28 -04:00
err := s . store . Update ( func ( tx store . Tx ) error {
service = store . GetService ( tx , request . ServiceID )
if service == nil {
return nil
}
2016-06-17 22:01:18 -04:00
// temporary disable network update
2016-10-03 13:58:05 -04:00
requestSpecNetworks := request . Spec . Task . Networks
if len ( requestSpecNetworks ) == 0 {
requestSpecNetworks = request . Spec . Networks
}
2016-08-23 01:30:01 -04:00
2016-10-03 13:58:05 -04:00
specNetworks := service . Spec . Task . Networks
if len ( specNetworks ) == 0 {
specNetworks = service . Spec . Networks
}
2016-08-23 01:30:01 -04:00
2016-10-03 13:58:05 -04:00
if ! reflect . DeepEqual ( requestSpecNetworks , specNetworks ) {
return errNetworkUpdateNotSupported
2016-06-17 22:01:18 -04:00
}
2016-11-04 15:11:41 -04:00
// Check to see if all the secrets being added exist as objects
// in our datastore
err := s . checkSecretExistence ( tx , request . Spec )
if err != nil {
return err
}
2016-06-30 16:34:48 -04:00
// orchestrator is designed to be stateless, so it should not deal
// with service mode change (comparing current config with previous config).
// proper way to change service mode is to delete and re-add.
2016-10-03 13:58:05 -04:00
if reflect . TypeOf ( service . Spec . Mode ) != reflect . TypeOf ( request . Spec . Mode ) {
2016-06-30 16:34:48 -04:00
return errModeChangeNotAllowed
}
2016-10-26 09:35:48 -04:00
if service . Spec . Annotations . Name != request . Spec . Annotations . Name {
return errRenameNotSupported
}
2016-06-07 17:28:28 -04:00
service . Meta . Version = * request . ServiceVersion
2016-09-13 12:28:01 -04:00
service . PreviousSpec = service . Spec . Copy ( )
2016-06-07 17:28:28 -04:00
service . Spec = * request . Spec . Copy ( )
2016-07-22 13:26:45 -04:00
// Reset update status
service . UpdateStatus = nil
2016-06-07 17:28:28 -04:00
return store . UpdateService ( tx , service )
} )
if err != nil {
return nil , err
}
if service == nil {
return nil , grpc . Errorf ( codes . NotFound , "service %s not found" , request . ServiceID )
}
return & api . UpdateServiceResponse {
Service : service ,
} , nil
}
// RemoveService removes a Service referenced by ServiceID.
// - Returns `InvalidArgument` if ServiceID is not provided.
// - Returns `NotFound` if the Service is not found.
// - Returns an error if the deletion fails.
func ( s * Server ) RemoveService ( ctx context . Context , request * api . RemoveServiceRequest ) ( * api . RemoveServiceResponse , error ) {
if request . ServiceID == "" {
return nil , grpc . Errorf ( codes . InvalidArgument , errInvalidArgument . Error ( ) )
}
err := s . store . Update ( func ( tx store . Tx ) error {
return store . DeleteService ( tx , request . ServiceID )
} )
if err != nil {
if err == store . ErrNotExist {
return nil , grpc . Errorf ( codes . NotFound , "service %s not found" , request . ServiceID )
}
return nil , err
}
return & api . RemoveServiceResponse { } , nil
}
func filterServices ( candidates [ ] * api . Service , filters ... func ( * api . Service ) bool ) [ ] * api . Service {
result := [ ] * api . Service { }
for _ , c := range candidates {
match := true
for _ , f := range filters {
if ! f ( c ) {
match = false
break
}
}
if match {
result = append ( result , c )
}
}
return result
}
// ListServices returns a list of all services.
func ( s * Server ) ListServices ( ctx context . Context , request * api . ListServicesRequest ) ( * api . ListServicesResponse , error ) {
var (
services [ ] * api . Service
err error
)
s . store . View ( func ( tx store . ReadTx ) {
switch {
case request . Filters != nil && len ( request . Filters . Names ) > 0 :
services , err = store . FindServices ( tx , buildFilters ( store . ByName , request . Filters . Names ) )
2016-07-20 11:16:54 -04:00
case request . Filters != nil && len ( request . Filters . NamePrefixes ) > 0 :
services , err = store . FindServices ( tx , buildFilters ( store . ByNamePrefix , request . Filters . NamePrefixes ) )
2016-06-07 17:28:28 -04:00
case request . Filters != nil && len ( request . Filters . IDPrefixes ) > 0 :
services , err = store . FindServices ( tx , buildFilters ( store . ByIDPrefix , request . Filters . IDPrefixes ) )
default :
services , err = store . FindServices ( tx , store . All )
}
} )
if err != nil {
return nil , err
}
if request . Filters != nil {
services = filterServices ( services ,
func ( e * api . Service ) bool {
return filterContains ( e . Spec . Annotations . Name , request . Filters . Names )
} ,
2016-07-20 11:16:54 -04:00
func ( e * api . Service ) bool {
return filterContainsPrefix ( e . Spec . Annotations . Name , request . Filters . NamePrefixes )
} ,
2016-06-07 17:28:28 -04:00
func ( e * api . Service ) bool {
return filterContainsPrefix ( e . ID , request . Filters . IDPrefixes )
} ,
func ( e * api . Service ) bool {
return filterMatchLabels ( e . Spec . Annotations . Labels , request . Filters . Labels )
} ,
)
}
return & api . ListServicesResponse {
Services : services ,
} , nil
}