2016-06-07 21:28:28 +00:00
package controlapi
import (
2018-10-11 21:03:18 +00:00
"context"
2016-06-18 02:01:18 +00:00
"errors"
"reflect"
2016-11-04 19:11:41 +00:00
"strings"
2017-02-16 16:50:00 +00:00
"time"
2016-06-18 02:01:18 +00:00
2016-09-13 16:28:01 +00:00
"github.com/docker/distribution/reference"
2016-06-07 21:28:28 +00:00
"github.com/docker/swarmkit/api"
2017-03-30 23:12:33 +00:00
"github.com/docker/swarmkit/api/defaults"
2017-06-12 17:27:11 +00:00
"github.com/docker/swarmkit/api/genericresource"
2017-03-28 18:51:33 +00:00
"github.com/docker/swarmkit/api/naming"
2016-06-07 21:28:28 +00:00
"github.com/docker/swarmkit/identity"
2017-03-13 21:45:06 +00:00
"github.com/docker/swarmkit/manager/allocator"
2016-10-15 15:49:04 +00:00
"github.com/docker/swarmkit/manager/constraint"
2016-06-07 21:28:28 +00:00
"github.com/docker/swarmkit/manager/state/store"
2017-02-16 16:50:00 +00:00
"github.com/docker/swarmkit/protobuf/ptypes"
2016-11-09 22:28:06 +00:00
"github.com/docker/swarmkit/template"
2017-01-23 23:50:10 +00:00
gogotypes "github.com/gogo/protobuf/types"
2016-06-07 21:28:28 +00:00
"google.golang.org/grpc/codes"
2017-12-05 00:38:37 +00:00
"google.golang.org/grpc/status"
2016-06-07 21:28:28 +00:00
)
2016-06-18 02:01:18 +00:00
var (
2017-03-28 18:51:33 +00:00
errNetworkUpdateNotSupported = errors . New ( "networks must be migrated to TaskSpec before being changed" )
2016-10-26 13:35:48 +00:00
errRenameNotSupported = errors . New ( "renaming services is not supported" )
2016-06-30 20:34:48 +00:00
errModeChangeNotAllowed = errors . New ( "service mode change is not allowed" )
2016-06-18 02:01:18 +00:00
)
2017-04-26 18:01:01 +00:00
const minimumDuration = 1 * time . Millisecond
2016-06-07 21:28:28 +00:00
func validateResources ( r * api . Resources ) error {
if r == nil {
return nil
}
if r . NanoCPUs != 0 && r . NanoCPUs < 1e6 {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "invalid cpu value %g: Must be at least %g" , float64 ( r . NanoCPUs ) / 1e9 , 1e6 / 1e9 )
2016-06-07 21:28:28 +00:00
}
if r . MemoryBytes != 0 && r . MemoryBytes < 4 * 1024 * 1024 {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "invalid memory value %d: Must be at least 4MiB" , r . MemoryBytes )
2016-06-07 21:28:28 +00:00
}
2017-06-12 17:27:11 +00:00
if err := genericresource . ValidateTask ( r ) ; err != nil {
return nil
}
2016-06-07 21:28:28 +00:00
return nil
}
func validateResourceRequirements ( r * api . ResourceRequirements ) error {
if r == nil {
return nil
}
if err := validateResources ( r . Limits ) ; err != nil {
return err
}
2017-09-26 22:07:16 +00:00
return validateResources ( r . Reservations )
2016-06-07 21:28:28 +00:00
}
2016-06-30 20:34:48 +00:00
func validateRestartPolicy ( rp * api . RestartPolicy ) error {
if rp == nil {
return nil
}
if rp . Delay != nil {
2017-01-23 23:50:10 +00:00
delay , err := gogotypes . DurationFromProto ( rp . Delay )
2016-06-30 20:34:48 +00:00
if err != nil {
return err
}
if delay < 0 {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "TaskSpec: restart-delay cannot be negative" )
2016-06-30 20:34:48 +00:00
}
}
if rp . Window != nil {
2017-01-23 23:50:10 +00:00
win , err := gogotypes . DurationFromProto ( rp . Window )
2016-06-30 20:34:48 +00:00
if err != nil {
return err
}
if win < 0 {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "TaskSpec: restart-window cannot be negative" )
2016-06-30 20:34:48 +00:00
}
}
return nil
}
2016-07-07 11:58:43 +00:00
func validatePlacement ( placement * api . Placement ) error {
if placement == nil {
return nil
}
2016-10-15 15:49:04 +00:00
_ , err := constraint . Parse ( placement . Constraints )
2016-07-07 11:58:43 +00:00
return err
}
2016-06-30 20:34:48 +00:00
func validateUpdate ( uc * api . UpdateConfig ) error {
if uc == nil {
return nil
}
2017-01-23 23:50:10 +00:00
if uc . Delay < 0 {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "TaskSpec: update-delay cannot be negative" )
2016-06-30 20:34:48 +00:00
}
2017-02-16 16:50:00 +00:00
if uc . Monitor != nil {
monitor , err := gogotypes . DurationFromProto ( uc . Monitor )
if err != nil {
return err
}
if monitor < 0 {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "TaskSpec: update-monitor cannot be negative" )
2017-02-16 16:50:00 +00:00
}
}
if uc . MaxFailureRatio < 0 || uc . MaxFailureRatio > 1 {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "TaskSpec: update-maxfailureratio cannot be less than 0 or bigger than 1" )
2017-02-16 16:50:00 +00:00
}
2016-06-30 20:34:48 +00:00
return nil
}
2017-03-28 18:51:33 +00:00
func validateContainerSpec ( taskSpec api . TaskSpec ) error {
// Building a empty/dummy Task to validate the templating and
// the resulting container spec as well. This is a *best effort*
// validation.
2017-07-11 19:22:34 +00:00
container , err := template . ExpandContainerSpec ( & api . NodeDescription {
Hostname : "nodeHostname" ,
Platform : & api . Platform {
OS : "os" ,
Architecture : "architecture" ,
} ,
} , & api . Task {
2017-03-28 18:51:33 +00:00
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 {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , err . Error ( ) )
2016-10-26 13:35:48 +00:00
}
2017-04-26 18:01:01 +00:00
if err := validateImage ( container . Image ) ; err != nil {
return err
}
if err := validateMounts ( container . Mounts ) ; err != nil {
return err
}
2017-09-26 22:07:16 +00:00
return validateHealthCheck ( container . Healthcheck )
2017-04-26 18:01:01 +00:00
}
// validateImage validates image name in containerSpec
func validateImage ( image string ) error {
if image == "" {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "ContainerSpec: image reference must be provided" )
2016-10-26 13:35:48 +00:00
}
2017-04-26 18:01:01 +00:00
if _ , err := reference . ParseNormalizedNamed ( image ) ; err != nil {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "ContainerSpec: %q is not a valid repository/tag" , image )
2016-10-26 13:35:48 +00:00
}
2017-04-26 18:01:01 +00:00
return nil
}
2016-10-26 13:35:48 +00:00
2017-04-26 18:01:01 +00:00
// validateMounts validates if there are duplicate mounts in containerSpec
func validateMounts ( mounts [ ] api . Mount ) error {
2016-10-26 13:35:48 +00:00
mountMap := make ( map [ string ] bool )
2017-04-26 18:01:01 +00:00
for _ , mount := range mounts {
2016-10-26 13:35:48 +00:00
if _ , exists := mountMap [ mount . Target ] ; exists {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "ContainerSpec: duplicate mount point: %s" , mount . Target )
2016-10-26 13:35:48 +00:00
}
mountMap [ mount . Target ] = true
}
return nil
}
2017-04-26 18:01:01 +00:00
// validateHealthCheck validates configs about container's health check
func validateHealthCheck ( hc * api . HealthConfig ) error {
if hc == nil {
return nil
}
if hc . Interval != nil {
interval , err := gogotypes . DurationFromProto ( hc . Interval )
if err != nil {
return err
}
2018-10-11 21:03:18 +00:00
if interval != 0 && interval < minimumDuration {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "ContainerSpec: Interval in HealthConfig cannot be less than %s" , minimumDuration )
2017-04-26 18:01:01 +00:00
}
}
if hc . Timeout != nil {
timeout , err := gogotypes . DurationFromProto ( hc . Timeout )
if err != nil {
return err
}
2018-10-11 21:03:18 +00:00
if timeout != 0 && timeout < minimumDuration {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "ContainerSpec: Timeout in HealthConfig cannot be less than %s" , minimumDuration )
2017-04-26 18:01:01 +00:00
}
}
if hc . StartPeriod != nil {
sp , err := gogotypes . DurationFromProto ( hc . StartPeriod )
if err != nil {
return err
}
2018-10-11 21:03:18 +00:00
if sp != 0 && sp < minimumDuration {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "ContainerSpec: StartPeriod in HealthConfig cannot be less than %s" , minimumDuration )
2017-04-26 18:01:01 +00:00
}
}
if hc . Retries < 0 {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "ContainerSpec: Retries in HealthConfig cannot be negative" )
2017-04-26 18:01:01 +00:00
}
return nil
}
2017-03-28 18:51:33 +00:00
func validateGenericRuntimeSpec ( taskSpec api . TaskSpec ) error {
generic := taskSpec . GetGeneric ( )
if len ( generic . Kind ) < 3 {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "Generic runtime: Invalid name %q" , generic . Kind )
2017-03-28 18:51:33 +00:00
}
reservedNames := [ ] string { "container" , "attachment" }
for _ , n := range reservedNames {
if strings . ToLower ( generic . Kind ) == n {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "Generic runtime: %q is a reserved name" , generic . Kind )
2017-03-28 18:51:33 +00:00
}
}
payload := generic . Payload
if payload == nil {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "Generic runtime is missing payload" )
2017-03-28 18:51:33 +00:00
}
if payload . TypeUrl == "" {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "Generic runtime is missing payload type" )
2017-03-28 18:51:33 +00:00
}
if len ( payload . Value ) == 0 {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "Generic runtime has an empty payload" )
2017-03-28 18:51:33 +00:00
}
return nil
}
2017-02-16 16:50:00 +00:00
func validateTaskSpec ( taskSpec api . TaskSpec ) error {
2016-06-30 20:34:48 +00:00
if err := validateResourceRequirements ( taskSpec . Resources ) ; err != nil {
2016-06-07 21:28:28 +00:00
return err
}
2016-06-30 20:34:48 +00:00
if err := validateRestartPolicy ( taskSpec . Restart ) ; err != nil {
return err
}
2016-07-07 11:58:43 +00:00
if err := validatePlacement ( taskSpec . Placement ) ; err != nil {
return err
}
2017-04-04 03:54:30 +00:00
// Check to see if the secret reference portion of the spec is valid
2017-02-16 16:50:00 +00:00
if err := validateSecretRefsSpec ( taskSpec ) ; err != nil {
return err
}
2017-04-04 03:54:30 +00:00
// Check to see if the config reference portion of the spec is valid
if err := validateConfigRefsSpec ( taskSpec ) ; err != nil {
return err
}
2016-06-30 20:34:48 +00:00
if taskSpec . GetRuntime ( ) == nil {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "TaskSpec: missing runtime" )
2016-06-07 21:28:28 +00:00
}
2017-03-28 18:51:33 +00:00
switch taskSpec . GetRuntime ( ) . ( type ) {
case * api . TaskSpec_Container :
if err := validateContainerSpec ( taskSpec ) ; err != nil {
return err
}
case * api . TaskSpec_Generic :
if err := validateGenericRuntimeSpec ( taskSpec ) ; err != nil {
return err
}
default :
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . Unimplemented , "RuntimeSpec: unimplemented runtime in service spec" )
2016-06-07 21:28:28 +00:00
}
2017-03-04 00:24:15 +00:00
2016-06-07 21:28:28 +00:00
return nil
}
2016-06-18 02:01:18 +00:00
func validateEndpointSpec ( epSpec * api . EndpointSpec ) error {
// Endpoint spec is optional
if epSpec == nil {
return nil
}
2016-10-20 18:26:04 +00:00
type portSpec struct {
publishedPort uint32
protocol api . PortConfig_Protocol
}
portSet := make ( map [ portSpec ] struct { } )
2016-06-18 02:01:18 +00:00
for _ , port := range epSpec . Ports {
2016-11-17 02:15:15 +00: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 {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "EndpointSpec: port published with ingress mode can't be used with dnsrr mode" )
2016-11-17 02:15:15 +00:00
}
2016-10-20 18:26:04 +00: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 {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "EndpointSpec: duplicate published ports provided" )
2016-06-18 02:01:18 +00:00
}
2016-10-20 18:26:04 +00:00
portSet [ portSpec ] = struct { } { }
2016-06-18 02:01:18 +00:00
}
return nil
}
2016-11-04 19:11:41 +00:00
// validateSecretRefsSpec finds if the secrets passed in spec are valid and have no
// conflicting targets.
2017-02-16 16:50:00 +00:00
func validateSecretRefsSpec ( spec api . TaskSpec ) error {
container := spec . GetContainer ( )
2016-11-04 19:11:41 +00:00
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 == "" {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "malformed secret reference" )
2016-11-04 19:11:41 +00:00
}
2017-01-23 23:50:10 +00:00
// Every secret reference requires a Target
2016-11-04 19:11:41 +00:00
if secretRef . GetTarget ( ) == nil {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "malformed secret reference, no target provided" )
2016-11-04 19:11:41 +00:00
}
// If this is a file target, we will ensure filename uniqueness
if secretRef . GetFile ( ) != nil {
fileName := secretRef . GetFile ( ) . Name
2017-04-26 18:01:01 +00:00
if fileName == "" {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "malformed file secret reference, invalid target file name provided" )
2016-11-04 19:11:41 +00:00
}
// If this target is already in use, we have conflicting targets
if prevSecretName , ok := existingTargets [ fileName ] ; ok {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "secret references '%s' and '%s' have a conflicting target: '%s'" , prevSecretName , secretRef . SecretName , fileName )
2016-11-04 19:11:41 +00:00
}
existingTargets [ fileName ] = secretRef . SecretName
}
}
return nil
}
2017-02-16 16:50:00 +00:00
2017-04-04 03:54:30 +00:00
// validateConfigRefsSpec finds if the configs passed in spec are valid and have no
// conflicting targets.
func validateConfigRefsSpec ( spec api . TaskSpec ) error {
container := spec . GetContainer ( )
if container == nil {
return nil
}
2019-03-26 13:57:19 +00:00
// check if we're using a config as a CredentialSpec -- if so, we need to
// verify
var (
credSpecConfig string
credSpecConfigFound bool
)
if p := container . Privileges ; p != nil {
if cs := p . CredentialSpec ; cs != nil {
// if there is no config in the credspec, then this will just be
// assigned to emptystring anyway, so we don't need to check
// existence.
credSpecConfig = cs . GetConfig ( )
}
}
2017-04-04 03:54:30 +00:00
// 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 _ , configRef := range container . Configs {
// ConfigID and ConfigName are mandatory, we have invalid references without them
if configRef . ConfigID == "" || configRef . ConfigName == "" {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "malformed config reference" )
2017-04-04 03:54:30 +00:00
}
// Every config reference requires a Target
if configRef . GetTarget ( ) == nil {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "malformed config reference, no target provided" )
2017-04-04 03:54:30 +00:00
}
// If this is a file target, we will ensure filename uniqueness
if configRef . GetFile ( ) != nil {
fileName := configRef . GetFile ( ) . Name
// Validate the file name
if fileName == "" {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "malformed file config reference, invalid target file name provided" )
2017-04-04 03:54:30 +00:00
}
// If this target is already in use, we have conflicting targets
if prevConfigName , ok := existingTargets [ fileName ] ; ok {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "config references '%s' and '%s' have a conflicting target: '%s'" , prevConfigName , configRef . ConfigName , fileName )
2017-04-04 03:54:30 +00:00
}
existingTargets [ fileName ] = configRef . ConfigName
}
2019-03-26 13:57:19 +00:00
if configRef . GetRuntime ( ) != nil {
if configRef . ConfigID == credSpecConfig {
credSpecConfigFound = true
}
}
}
if credSpecConfig != "" && ! credSpecConfigFound {
return status . Errorf (
codes . InvalidArgument ,
"CredentialSpec references config '%s', but that config isn't in config references with RuntimeTarget" ,
credSpecConfig ,
)
2017-04-04 03:54:30 +00:00
}
return nil
}
2016-10-15 15:49:04 +00: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
}
2017-05-11 22:18:12 +00:00
if allocator . IsIngressNetwork ( network ) {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument ,
2017-05-11 22:18:12 +00:00
"Service cannot be explicitly attached to the ingress network %q" , network . Spec . Annotations . Name )
2016-10-15 15:49:04 +00:00
}
}
return nil
}
2017-02-07 00:23:19 +00:00
func validateMode ( s * api . ServiceSpec ) error {
m := s . GetMode ( )
2019-12-11 16:05:03 +00:00
switch mode := m . ( type ) {
2017-02-07 00:23:19 +00:00
case * api . ServiceSpec_Replicated :
2019-12-11 16:05:03 +00:00
if int64 ( mode . Replicated . Replicas ) < 0 {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "Number of replicas must be non-negative" )
2017-02-07 00:23:19 +00:00
}
case * api . ServiceSpec_Global :
2019-12-11 16:05:03 +00:00
case * api . ServiceSpec_ReplicatedJob :
// this check shouldn't be required as the point of uint64 is to
// constrain the possible values to positive numbers, but it almost
// certainly is required because there are almost certainly blind casts
// from int64 to uint64, and uint64(-1) is almost certain to crash the
// cluster because of how large it is.
if int64 ( mode . ReplicatedJob . MaxConcurrent ) < 0 {
return status . Errorf (
codes . InvalidArgument ,
"Maximum concurrent jobs must not be negative" ,
)
}
if int64 ( mode . ReplicatedJob . TotalCompletions ) < 0 {
return status . Errorf (
codes . InvalidArgument ,
"Total completed jobs must not be negative" ,
)
}
case * api . ServiceSpec_GlobalJob :
2017-02-07 00:23:19 +00:00
default :
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "Unrecognized service mode" )
2017-02-07 00:23:19 +00:00
}
return nil
}
2019-12-11 16:05:03 +00:00
func validateJob ( spec * api . ServiceSpec ) error {
if spec . Update != nil {
return status . Errorf ( codes . InvalidArgument , "Jobs may not have an update config" )
}
return nil
}
2016-06-07 21:28:28 +00:00
func validateServiceSpec ( spec * api . ServiceSpec ) error {
if spec == nil {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , errInvalidArgument . Error ( ) )
2016-06-07 21:28:28 +00:00
}
if err := validateAnnotations ( spec . Annotations ) ; err != nil {
return err
}
2017-02-16 16:50:00 +00:00
if err := validateTaskSpec ( spec . Task ) ; err != nil {
2016-06-30 20:34:48 +00:00
return err
}
2019-12-11 16:05:03 +00:00
err := validateMode ( spec )
if err != nil {
2016-06-30 20:34:48 +00:00
return err
}
2019-12-11 16:05:03 +00:00
// job-mode services are validated differently. most notably, they do not
// have UpdateConfigs, which is why this case statement skips update
// validation.
if isJobSpec ( spec ) {
if err := validateJob ( spec ) ; err != nil {
return err
}
} else {
if err := validateUpdate ( spec . Update ) ; err != nil {
return err
}
2016-06-07 21:28:28 +00:00
}
2019-12-11 16:05:03 +00:00
return validateEndpointSpec ( spec . Endpoint )
}
func isJobSpec ( spec * api . ServiceSpec ) bool {
mode := spec . GetMode ( )
_ , isGlobalJob := mode . ( * api . ServiceSpec_GlobalJob )
_ , isReplicatedJob := mode . ( * api . ServiceSpec_ReplicatedJob )
return isGlobalJob || isReplicatedJob
2016-06-07 21:28:28 +00:00
}
2016-07-22 17:26:45 +00:00
// checkPortConflicts does a best effort to find if the passed in spec has port
// conflicts with existing services.
2016-08-11 23:41:44 +00: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 18:49:39 +00:00
func ( s * Server ) checkPortConflicts ( spec * api . ServiceSpec , serviceID string ) error {
2016-07-22 17:26:45 +00:00
if spec . Endpoint == nil {
return nil
}
2017-05-11 22:18:12 +00:00
type portSpec struct {
protocol api . PortConfig_Protocol
publishedPort uint32
2016-07-22 17:26:45 +00:00
}
2017-05-11 22:18:12 +00:00
pcToStruct := func ( pc * api . PortConfig ) portSpec {
return portSpec {
protocol : pc . Protocol ,
publishedPort : pc . PublishedPort ,
}
}
ingressPorts := make ( map [ portSpec ] struct { } )
hostModePorts := make ( map [ portSpec ] struct { } )
2016-07-22 17:26:45 +00:00
for _ , pc := range spec . Endpoint . Ports {
2017-05-11 22:18:12 +00:00
if pc . PublishedPort == 0 {
continue
}
switch pc . PublishMode {
case api . PublishModeIngress :
ingressPorts [ pcToStruct ( pc ) ] = struct { } { }
case api . PublishModeHost :
hostModePorts [ pcToStruct ( pc ) ] = struct { } { }
2016-07-22 17:26:45 +00:00
}
}
2017-05-11 22:18:12 +00:00
if len ( ingressPorts ) == 0 && len ( hostModePorts ) == 0 {
2016-07-22 17:26:45 +00:00
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
}
2017-05-11 22:18:12 +00:00
isPortInUse := func ( pc * api . PortConfig , service * api . Service ) error {
if pc . PublishedPort == 0 {
return nil
}
switch pc . PublishMode {
case api . PublishModeHost :
if _ , ok := ingressPorts [ pcToStruct ( pc ) ] ; ok {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "port '%d' is already in use by service '%s' (%s) as a host-published port" , pc . PublishedPort , service . Spec . Annotations . Name , service . ID )
2017-05-11 22:18:12 +00:00
}
// Multiple services with same port in host publish mode can
// coexist - this is handled by the scheduler.
return nil
case api . PublishModeIngress :
_ , ingressConflict := ingressPorts [ pcToStruct ( pc ) ]
_ , hostModeConflict := hostModePorts [ pcToStruct ( pc ) ]
if ingressConflict || hostModeConflict {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "port '%d' is already in use by service '%s' (%s) as an ingress port" , pc . PublishedPort , service . Spec . Annotations . Name , service . ID )
2017-05-11 22:18:12 +00:00
}
}
return nil
}
2016-07-22 17:26:45 +00:00
for _ , service := range services {
2016-08-09 18:49:39 +00:00
// If service ID is the same (and not "") then this is an update
if serviceID != "" && serviceID == service . ID {
continue
}
2016-07-22 17:26:45 +00:00
if service . Spec . Endpoint != nil {
for _ , pc := range service . Spec . Endpoint . Ports {
2017-05-11 22:18:12 +00:00
if err := isPortInUse ( pc , service ) ; err != nil {
return err
2016-07-22 17:26:45 +00:00
}
}
}
if service . Endpoint != nil {
for _ , pc := range service . Endpoint . Ports {
2017-05-11 22:18:12 +00:00
if err := isPortInUse ( pc , service ) ; err != nil {
return err
2016-07-22 17:26:45 +00:00
}
}
}
}
return nil
}
2016-11-04 19:11:41 +00:00
// checkSecretExistence finds if the secret exists
func ( s * Server ) checkSecretExistence ( tx store . Tx , spec * api . ServiceSpec ) error {
2016-10-26 13:35:48 +00:00
container := spec . Task . GetContainer ( )
if container == nil {
return nil
}
2016-11-04 19:11:41 +00:00
var failedSecrets [ ] string
2016-10-26 13:35:48 +00:00
for _ , secretRef := range container . Secrets {
2016-11-04 19:11:41 +00: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 13:35:48 +00:00
}
2016-11-04 19:11:41 +00:00
}
2016-10-26 13:35:48 +00:00
2016-11-04 19:11:41 +00:00
if len ( failedSecrets ) > 0 {
secretStr := "secrets"
if len ( failedSecrets ) == 1 {
secretStr = "secret"
2016-11-02 18:43:27 +00:00
}
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "%s not found: %v" , secretStr , strings . Join ( failedSecrets , ", " ) )
2016-11-02 18:43:27 +00:00
2016-10-26 13:35:48 +00:00
}
return nil
}
2017-04-04 03:54:30 +00:00
// checkConfigExistence finds if the config exists
func ( s * Server ) checkConfigExistence ( tx store . Tx , spec * api . ServiceSpec ) error {
container := spec . Task . GetContainer ( )
if container == nil {
return nil
}
var failedConfigs [ ] string
for _ , configRef := range container . Configs {
config := store . GetConfig ( tx , configRef . ConfigID )
// Check to see if the config exists and configRef.ConfigName matches the actual configName
if config == nil || config . Spec . Annotations . Name != configRef . ConfigName {
failedConfigs = append ( failedConfigs , configRef . ConfigName )
}
}
if len ( failedConfigs ) > 0 {
configStr := "configs"
if len ( failedConfigs ) == 1 {
configStr = "config"
}
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . InvalidArgument , "%s not found: %v" , configStr , strings . Join ( failedConfigs , ", " ) )
2017-04-04 03:54:30 +00:00
}
return nil
}
2017-02-16 16:50:00 +00:00
// CreateService creates and returns a Service based on the provided ServiceSpec.
2016-06-07 21:28:28 +00:00
// - 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
}
2018-03-26 19:29:15 +00:00
if err := s . validateNetworks ( request . Spec . Task . Networks ) ; err != nil {
2016-10-15 15:49:04 +00:00
return nil , err
}
2016-08-09 18:49:39 +00:00
if err := s . checkPortConflicts ( request . Spec , "" ) ; err != nil {
2016-07-22 17:26:45 +00:00
return nil , err
}
2016-06-07 21:28:28 +00:00
// TODO(aluzzardi): Consider using `Name` as a primary key to handle
// duplicate creations. See #65
service := & api . Service {
2017-03-28 18:51:33 +00:00
ID : identity . NewID ( ) ,
Spec : * request . Spec ,
SpecVersion : & api . Version { } ,
2016-06-07 21:28:28 +00:00
}
2019-12-11 16:05:03 +00:00
if isJobSpec ( request . Spec ) {
service . JobStatus = & api . JobStatus {
LastExecution : gogotypes . TimestampNow ( ) ,
}
}
2017-03-28 18:51:33 +00:00
if allocator . IsIngressNetworkNeeded ( service ) {
2017-03-13 21:45:06 +00:00
if _ , err := allocator . GetIngressNetwork ( s . store ) ; err == allocator . ErrNoIngress {
2017-12-05 00:38:37 +00:00
return nil , status . Errorf ( codes . FailedPrecondition , "service needs ingress network, but no ingress network is present" )
2017-03-13 21:45:06 +00:00
}
}
2016-06-07 21:28:28 +00:00
err := s . store . Update ( func ( tx store . Tx ) error {
2016-11-04 19:11:41 +00: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
}
2017-04-04 03:54:30 +00:00
err = s . checkConfigExistence ( tx , request . Spec )
if err != nil {
return err
}
2016-11-04 19:11:41 +00:00
2016-06-07 21:28:28 +00:00
return store . CreateService ( tx , service )
} )
2018-11-20 21:48:47 +00:00
switch err {
case store . ErrNameConflict :
2019-02-01 00:21:43 +00:00
// Enhance the name-confict error to include the service name. The original
// `ErrNameConflict` error-message is included for backward-compatibility
// with older consumers of the API performing string-matching.
return nil , status . Errorf ( codes . AlreadyExists , "%s: service %s already exists" , err . Error ( ) , request . Spec . Annotations . Name )
2018-11-20 21:48:47 +00:00
case nil :
return & api . CreateServiceResponse { Service : service } , nil
default :
2016-06-07 21:28:28 +00:00
return nil , err
}
}
// 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 == "" {
2017-12-05 00:38:37 +00:00
return nil , status . Errorf ( codes . InvalidArgument , errInvalidArgument . Error ( ) )
2016-06-07 21:28:28 +00:00
}
var service * api . Service
s . store . View ( func ( tx store . ReadTx ) {
service = store . GetService ( tx , request . ServiceID )
} )
if service == nil {
2017-12-05 00:38:37 +00:00
return nil , status . Errorf ( codes . NotFound , "service %s not found" , request . ServiceID )
2016-06-07 21:28:28 +00:00
}
2017-03-30 23:12:33 +00:00
if request . InsertDefaults {
service . Spec = * defaults . InterpolateService ( & service . Spec )
}
2016-06-07 21:28:28 +00:00
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 {
2017-12-05 00:38:37 +00:00
return nil , status . Errorf ( codes . InvalidArgument , errInvalidArgument . Error ( ) )
2016-06-07 21:28:28 +00:00
}
if err := validateServiceSpec ( request . Spec ) ; err != nil {
return nil , err
}
2018-03-26 19:29:15 +00:00
if err := s . validateNetworks ( request . Spec . Task . Networks ) ; err != nil {
return nil , err
}
2016-06-07 21:28:28 +00:00
var service * api . Service
2016-07-22 17:26:45 +00:00
s . store . View ( func ( tx store . ReadTx ) {
service = store . GetService ( tx , request . ServiceID )
} )
if service == nil {
2017-12-05 00:38:37 +00:00
return nil , status . Errorf ( codes . NotFound , "service %s not found" , request . ServiceID )
2016-07-22 17:26:45 +00:00
}
if request . Spec . Endpoint != nil && ! reflect . DeepEqual ( request . Spec . Endpoint , service . Spec . Endpoint ) {
2016-08-09 18:49:39 +00:00
if err := s . checkPortConflicts ( request . Spec , request . ServiceID ) ; err != nil {
2016-07-22 17:26:45 +00:00
return nil , err
}
}
2016-06-07 21:28:28 +00:00
err := s . store . Update ( func ( tx store . Tx ) error {
service = store . GetService ( tx , request . ServiceID )
if service == nil {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . NotFound , "service %s not found" , request . ServiceID )
2016-06-07 21:28:28 +00:00
}
2016-08-23 05:30:01 +00:00
2017-03-28 18:51:33 +00:00
// It's not okay to update Service.Spec.Networks on its own.
// However, if Service.Spec.Task.Networks is also being
// updated, that's okay (for example when migrating from the
// deprecated Spec.Networks field to Spec.Task.Networks).
if ( len ( request . Spec . Networks ) != 0 || len ( service . Spec . Networks ) != 0 ) &&
! reflect . DeepEqual ( request . Spec . Networks , service . Spec . Networks ) &&
reflect . DeepEqual ( request . Spec . Task . Networks , service . Spec . Task . Networks ) {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . Unimplemented , errNetworkUpdateNotSupported . Error ( ) )
2016-06-18 02:01:18 +00:00
}
2016-11-04 19:11:41 +00: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
}
2017-04-04 03:54:30 +00:00
err = s . checkConfigExistence ( tx , request . Spec )
if err != nil {
return err
}
2016-06-30 20:34:48 +00: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 17:58:05 +00:00
if reflect . TypeOf ( service . Spec . Mode ) != reflect . TypeOf ( request . Spec . Mode ) {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . Unimplemented , errModeChangeNotAllowed . Error ( ) )
2016-06-30 20:34:48 +00:00
}
2016-10-26 13:35:48 +00:00
if service . Spec . Annotations . Name != request . Spec . Annotations . Name {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . Unimplemented , errRenameNotSupported . Error ( ) )
2016-10-26 13:35:48 +00:00
}
2016-06-07 21:28:28 +00:00
service . Meta . Version = * request . ServiceVersion
2016-07-22 17:26:45 +00:00
2019-12-11 16:05:03 +00:00
// if the service has a JobStatus, that means it must be a Job, and we
// should increment the JobIteration
if service . JobStatus != nil {
service . JobStatus . JobIteration . Index = service . JobStatus . JobIteration . Index + 1
service . JobStatus . LastExecution = gogotypes . TimestampNow ( )
}
2017-02-16 16:50:00 +00:00
if request . Rollback == api . UpdateServiceRequest_PREVIOUS {
if service . PreviousSpec == nil {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . FailedPrecondition , "service %s does not have a previous spec" , request . ServiceID )
2017-02-16 16:50:00 +00:00
}
curSpec := service . Spec . Copy ( )
2017-03-28 18:51:33 +00:00
curSpecVersion := service . SpecVersion
2017-02-16 16:50:00 +00:00
service . Spec = * service . PreviousSpec . Copy ( )
2017-03-28 18:51:33 +00:00
service . SpecVersion = service . PreviousSpecVersion . Copy ( )
2017-02-16 16:50:00 +00:00
service . PreviousSpec = curSpec
2017-03-28 18:51:33 +00:00
service . PreviousSpecVersion = curSpecVersion
2017-02-16 16:50:00 +00:00
service . UpdateStatus = & api . UpdateStatus {
State : api . UpdateStatus_ROLLBACK_STARTED ,
Message : "manually requested rollback" ,
StartedAt : ptypes . MustTimestampProto ( time . Now ( ) ) ,
}
} else {
service . PreviousSpec = service . Spec . Copy ( )
2017-03-28 18:51:33 +00:00
service . PreviousSpecVersion = service . SpecVersion
2017-02-16 16:50:00 +00:00
service . Spec = * request . Spec . Copy ( )
2017-03-28 18:51:33 +00:00
// Set spec version. Note that this will not match the
// service's Meta.Version after the store update. The
2017-04-04 03:54:30 +00:00
// versions for the spec and the service itself are not
2017-03-28 18:51:33 +00:00
// meant to be directly comparable.
service . SpecVersion = service . Meta . Version . Copy ( )
2017-02-16 16:50:00 +00:00
// Reset update status
service . UpdateStatus = nil
}
2016-07-22 17:26:45 +00:00
2017-03-28 18:51:33 +00:00
if allocator . IsIngressNetworkNeeded ( service ) {
2017-03-13 21:45:06 +00:00
if _ , err := allocator . GetIngressNetwork ( s . store ) ; err == allocator . ErrNoIngress {
2017-12-05 00:38:37 +00:00
return status . Errorf ( codes . FailedPrecondition , "service needs ingress network, but no ingress network is present" )
2017-03-13 21:45:06 +00:00
}
}
2016-06-07 21:28:28 +00:00
return store . UpdateService ( tx , service )
} )
if err != nil {
return nil , err
}
2016-12-05 04:56:40 +00:00
2016-06-07 21:28:28 +00:00
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 == "" {
2017-12-05 00:38:37 +00:00
return nil , status . Errorf ( codes . InvalidArgument , errInvalidArgument . Error ( ) )
2016-06-07 21:28:28 +00:00
}
err := s . store . Update ( func ( tx store . Tx ) error {
return store . DeleteService ( tx , request . ServiceID )
} )
if err != nil {
if err == store . ErrNotExist {
2017-12-05 00:38:37 +00:00
return nil , status . Errorf ( codes . NotFound , "service %s not found" , request . ServiceID )
2016-06-07 21:28:28 +00:00
}
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 15:16:54 +00:00
case request . Filters != nil && len ( request . Filters . NamePrefixes ) > 0 :
services , err = store . FindServices ( tx , buildFilters ( store . ByNamePrefix , request . Filters . NamePrefixes ) )
2016-06-07 21:28:28 +00:00
case request . Filters != nil && len ( request . Filters . IDPrefixes ) > 0 :
services , err = store . FindServices ( tx , buildFilters ( store . ByIDPrefix , request . Filters . IDPrefixes ) )
2017-03-28 18:51:33 +00:00
case request . Filters != nil && len ( request . Filters . Runtimes ) > 0 :
services , err = store . FindServices ( tx , buildFilters ( store . ByRuntime , request . Filters . Runtimes ) )
2016-06-07 21:28:28 +00:00
default :
services , err = store . FindServices ( tx , store . All )
}
} )
if err != nil {
2018-11-20 21:48:47 +00:00
switch err {
case store . ErrInvalidFindBy :
return nil , status . Errorf ( codes . InvalidArgument , err . Error ( ) )
default :
return nil , err
}
2016-06-07 21:28:28 +00:00
}
if request . Filters != nil {
services = filterServices ( services ,
func ( e * api . Service ) bool {
return filterContains ( e . Spec . Annotations . Name , request . Filters . Names )
} ,
2016-07-20 15:16:54 +00:00
func ( e * api . Service ) bool {
return filterContainsPrefix ( e . Spec . Annotations . Name , request . Filters . NamePrefixes )
} ,
2016-06-07 21:28:28 +00: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 )
} ,
2017-03-28 18:51:33 +00:00
func ( e * api . Service ) bool {
if len ( request . Filters . Runtimes ) == 0 {
return true
}
r , err := naming . Runtime ( e . Spec . Task )
if err != nil {
return false
}
return filterContains ( r , request . Filters . Runtimes )
} ,
2016-06-07 21:28:28 +00:00
)
}
return & api . ListServicesResponse {
Services : services ,
} , nil
}
2019-05-24 16:56:56 +00:00
// ListServiceStatuses returns a `ListServiceStatusesResponse` with the status
// of the requested services, formed by computing the number of running vs
// desired tasks. It is provided as a shortcut or helper method, which allows a
// client to avoid having to calculate this value by listing all Tasks. If any
// service requested does not exist, it will be returned but with empty status
// values.
func ( s * Server ) ListServiceStatuses ( ctx context . Context , req * api . ListServiceStatusesRequest ) ( * api . ListServiceStatusesResponse , error ) {
resp := & api . ListServiceStatusesResponse { }
if req == nil {
return resp , nil
}
s . store . View ( func ( tx store . ReadTx ) {
for _ , id := range req . Services {
status := & api . ListServiceStatusesResponse_ServiceStatus {
ServiceID : id ,
}
// no matter what, add this status to the list.
resp . Statuses = append ( resp . Statuses , status )
tasks , findErr := store . FindTasks ( tx , store . ByServiceID ( id ) )
if findErr != nil {
// if there is another kind of error here (not sure what it
// could be) then still return 0/0 for this service.
continue
}
// use a boolean to see global vs replicated. this avoids us having to
// iterate the task list twice.
global := false
2019-12-11 16:05:03 +00:00
// jobIteration is the iteration that the Job is currently
// operating on, to distinguish Tasks in old executions from tasks
// in the current one. if nil, service is not a Job
var jobIteration * api . Version
2019-05-24 16:56:56 +00:00
service := store . GetService ( tx , id )
// a service might be deleted, but it may still have tasks. in that
// case, we will be using 0 as the desired task count.
if service != nil {
// figure out how many tasks the service requires. for replicated
// services, this is easy: we can just check the replicas field. for
// global services, this is a bit harder and we'll need to do some
// numbercrunchin
if replicated := service . Spec . GetReplicated ( ) ; replicated != nil {
status . DesiredTasks = replicated . Replicas
2019-12-11 16:05:03 +00:00
} else if replicatedJob := service . Spec . GetReplicatedJob ( ) ; replicatedJob != nil {
status . DesiredTasks = replicatedJob . MaxConcurrent
2019-05-24 16:56:56 +00:00
} else {
2019-12-11 16:05:03 +00:00
// global applies to both GlobalJob and regular Global
2019-05-24 16:56:56 +00:00
global = true
}
2019-12-11 16:05:03 +00:00
if service . JobStatus != nil {
jobIteration = & service . JobStatus . JobIteration
}
2019-05-24 16:56:56 +00:00
}
// now, figure out how many tasks are running. Pretty easy, and
// universal across both global and replicated services
for _ , task := range tasks {
2019-12-11 16:05:03 +00:00
// if the service is a Job, jobIteration will be non-nil. This
// means we should check if the task belongs to the current job
// iteration. If not, skip accounting the task.
if jobIteration != nil {
if task . JobIteration == nil || task . JobIteration . Index != jobIteration . Index {
continue
}
// additionally, since we've verified that the service is a
// job and the task belongs to this iteration, we should
// increment CompletedTasks
if task . Status . State == api . TaskStateCompleted {
status . CompletedTasks ++
}
}
2019-05-24 16:56:56 +00:00
if task . Status . State == api . TaskStateRunning {
status . RunningTasks ++
}
2019-12-11 16:05:03 +00:00
// if the service is global, a shortcut for figuring out the
2019-05-24 16:56:56 +00:00
// number of tasks desired is to look at all tasks, and take a
// count of the ones whose desired state is not Shutdown.
if global && task . DesiredState == api . TaskStateRunning {
status . DesiredTasks ++
}
2019-12-11 16:05:03 +00:00
// for jobs, this is any task with desired state Completed
// which is not actually in that state.
if global && task . Status . State != api . TaskStateCompleted && task . DesiredState == api . TaskStateCompleted {
status . DesiredTasks ++
}
2019-05-24 16:56:56 +00:00
}
}
} )
return resp , nil
}