2016-06-13 22:56:23 -04:00
package service
import (
"fmt"
2016-08-04 23:51:28 -04:00
"sort"
2016-06-20 12:57:57 -04:00
"strings"
2016-06-13 22:56:23 -04:00
"time"
"golang.org/x/net/context"
"github.com/docker/docker/api/client"
"github.com/docker/docker/cli"
"github.com/docker/docker/opts"
runconfigopts "github.com/docker/docker/runconfig/opts"
2016-07-05 11:20:49 -04:00
"github.com/docker/engine-api/types"
2016-08-15 12:13:18 -04:00
mounttypes "github.com/docker/engine-api/types/mount"
2016-06-13 22:56:23 -04:00
"github.com/docker/engine-api/types/swarm"
"github.com/docker/go-connections/nat"
2016-06-24 15:03:26 -04:00
shlex "github.com/flynn-archive/go-shlex"
2016-06-13 22:56:23 -04:00
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func newUpdateCommand ( dockerCli * client . DockerCli ) * cobra . Command {
opts := newServiceOptions ( )
cmd := & cobra . Command {
Use : "update [OPTIONS] SERVICE" ,
Short : "Update a service" ,
Args : cli . ExactArgs ( 1 ) ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2016-06-17 11:01:46 -04:00
return runUpdate ( dockerCli , cmd . Flags ( ) , args [ 0 ] )
2016-06-13 22:56:23 -04:00
} ,
}
2016-06-17 11:01:46 -04:00
flags := cmd . Flags ( )
2016-06-13 22:56:23 -04:00
flags . String ( "image" , "" , "Service image tag" )
2016-06-24 15:03:26 -04:00
flags . String ( "args" , "" , "Service command args" )
2016-06-13 22:56:23 -04:00
addServiceFlags ( cmd , opts )
2016-06-29 12:28:33 -04:00
flags . Var ( newListOptsVar ( ) , flagEnvRemove , "Remove an environment variable" )
flags . Var ( newListOptsVar ( ) , flagLabelRemove , "Remove a label by its key" )
2016-07-25 05:38:01 -04:00
flags . Var ( newListOptsVar ( ) , flagContainerLabelRemove , "Remove a container label by its key" )
2016-06-29 12:28:33 -04:00
flags . Var ( newListOptsVar ( ) , flagMountRemove , "Remove a mount by its target path" )
flags . Var ( newListOptsVar ( ) , flagPublishRemove , "Remove a published port by its target port" )
flags . Var ( newListOptsVar ( ) , flagConstraintRemove , "Remove a constraint" )
flags . Var ( & opts . labels , flagLabelAdd , "Add or update service labels" )
2016-07-25 05:38:01 -04:00
flags . Var ( & opts . containerLabels , flagContainerLabelAdd , "Add or update container labels" )
2016-06-29 12:28:33 -04:00
flags . Var ( & opts . env , flagEnvAdd , "Add or update environment variables" )
flags . Var ( & opts . mounts , flagMountAdd , "Add or update a mount on a service" )
flags . StringSliceVar ( & opts . constraints , flagConstraintAdd , [ ] string { } , "Add or update placement constraints" )
flags . Var ( & opts . endpoint . ports , flagPublishAdd , "Add or update a published port" )
2016-06-13 22:56:23 -04:00
return cmd
}
2016-06-29 12:28:33 -04:00
func newListOptsVar ( ) * opts . ListOpts {
return opts . NewListOptsRef ( & [ ] string { } , nil )
}
2016-06-13 22:56:23 -04:00
func runUpdate ( dockerCli * client . DockerCli , flags * pflag . FlagSet , serviceID string ) error {
2016-06-15 14:50:49 -04:00
apiClient := dockerCli . Client ( )
2016-06-13 22:56:23 -04:00
ctx := context . Background ( )
2016-07-05 11:20:49 -04:00
updateOpts := types . ServiceUpdateOptions { }
2016-06-13 22:56:23 -04:00
2016-06-29 20:08:00 -04:00
service , _ , err := apiClient . ServiceInspectWithRaw ( ctx , serviceID )
2016-06-13 22:56:23 -04:00
if err != nil {
return err
}
2016-06-29 12:38:23 -04:00
err = updateService ( flags , & service . Spec )
2016-06-13 22:56:23 -04:00
if err != nil {
return err
}
2016-06-29 20:08:00 -04:00
// only send auth if flag was set
sendAuth , err := flags . GetBool ( flagRegistryAuth )
2016-06-13 22:56:23 -04:00
if err != nil {
return err
}
2016-06-29 20:08:00 -04:00
if sendAuth {
// Retrieve encoded auth token from the image reference
// This would be the old image if it didn't change in this update
image := service . Spec . TaskTemplate . ContainerSpec . Image
encodedAuth , err := dockerCli . RetrieveAuthTokenFromImage ( ctx , image )
if err != nil {
return err
}
2016-07-05 11:20:49 -04:00
updateOpts . EncodedRegistryAuth = encodedAuth
2016-06-29 20:08:00 -04:00
}
2016-06-15 14:50:49 -04:00
2016-07-05 11:20:49 -04:00
err = apiClient . ServiceUpdate ( ctx , service . ID , service . Version , service . Spec , updateOpts )
2016-06-13 22:56:23 -04:00
if err != nil {
return err
}
fmt . Fprintf ( dockerCli . Out ( ) , "%s\n" , serviceID )
return nil
}
2016-06-29 12:38:23 -04:00
func updateService ( flags * pflag . FlagSet , spec * swarm . ServiceSpec ) error {
2016-06-17 19:24:07 -04:00
updateString := func ( flag string , field * string ) {
2016-06-13 22:56:23 -04:00
if flags . Changed ( flag ) {
* field , _ = flags . GetString ( flag )
}
}
2016-06-17 19:24:07 -04:00
updateInt64Value := func ( flag string , field * int64 ) {
2016-06-13 22:56:23 -04:00
if flags . Changed ( flag ) {
* field = flags . Lookup ( flag ) . Value . ( int64Value ) . Value ( )
}
}
2016-06-17 19:24:07 -04:00
updateDuration := func ( flag string , field * time . Duration ) {
2016-06-13 22:56:23 -04:00
if flags . Changed ( flag ) {
* field , _ = flags . GetDuration ( flag )
}
}
2016-07-26 02:53:20 -04:00
updateDurationOpt := func ( flag string , field * * time . Duration ) {
2016-06-13 22:56:23 -04:00
if flags . Changed ( flag ) {
2016-07-26 02:53:20 -04:00
val := * flags . Lookup ( flag ) . Value . ( * DurationOpt ) . Value ( )
* field = & val
2016-06-13 22:56:23 -04:00
}
}
2016-06-17 19:24:07 -04:00
updateUint64 := func ( flag string , field * uint64 ) {
2016-06-13 22:56:23 -04:00
if flags . Changed ( flag ) {
* field , _ = flags . GetUint64 ( flag )
}
}
2016-07-26 02:53:20 -04:00
updateUint64Opt := func ( flag string , field * * uint64 ) {
2016-06-13 22:56:23 -04:00
if flags . Changed ( flag ) {
2016-07-26 02:53:20 -04:00
val := * flags . Lookup ( flag ) . Value . ( * Uint64Opt ) . Value ( )
* field = & val
2016-06-13 22:56:23 -04:00
}
}
cspec := & spec . TaskTemplate . ContainerSpec
task := & spec . TaskTemplate
2016-06-17 11:01:46 -04:00
taskResources := func ( ) * swarm . ResourceRequirements {
if task . Resources == nil {
task . Resources = & swarm . ResourceRequirements { }
}
return task . Resources
}
2016-06-17 19:24:07 -04:00
updateString ( flagName , & spec . Name )
updateLabels ( flags , & spec . Labels )
2016-07-25 05:38:01 -04:00
updateContainerLabels ( flags , & cspec . Labels )
2016-06-17 19:24:07 -04:00
updateString ( "image" , & cspec . Image )
2016-06-24 15:03:26 -04:00
updateStringToSlice ( flags , "args" , & cspec . Args )
2016-06-20 12:57:57 -04:00
updateEnvironment ( flags , & cspec . Env )
2016-08-01 14:45:28 -04:00
updateString ( flagWorkdir , & cspec . Dir )
2016-06-17 11:01:46 -04:00
updateString ( flagUser , & cspec . User )
2016-06-17 19:24:07 -04:00
updateMounts ( flags , & cspec . Mounts )
2016-06-13 22:56:23 -04:00
2016-06-14 18:37:27 -04:00
if flags . Changed ( flagLimitCPU ) || flags . Changed ( flagLimitMemory ) {
2016-06-17 11:01:46 -04:00
taskResources ( ) . Limits = & swarm . Resources { }
2016-06-17 19:24:07 -04:00
updateInt64Value ( flagLimitCPU , & task . Resources . Limits . NanoCPUs )
updateInt64Value ( flagLimitMemory , & task . Resources . Limits . MemoryBytes )
2016-06-14 18:37:27 -04:00
}
if flags . Changed ( flagReserveCPU ) || flags . Changed ( flagReserveMemory ) {
2016-06-17 11:01:46 -04:00
taskResources ( ) . Reservations = & swarm . Resources { }
2016-06-17 19:24:07 -04:00
updateInt64Value ( flagReserveCPU , & task . Resources . Reservations . NanoCPUs )
updateInt64Value ( flagReserveMemory , & task . Resources . Reservations . MemoryBytes )
2016-06-14 18:37:27 -04:00
}
2016-06-13 22:56:23 -04:00
2016-07-26 02:53:20 -04:00
updateDurationOpt ( flagStopGracePeriod , & cspec . StopGracePeriod )
2016-06-13 22:56:23 -04:00
2016-06-17 11:01:46 -04:00
if anyChanged ( flags , flagRestartCondition , flagRestartDelay , flagRestartMaxAttempts , flagRestartWindow ) {
2016-06-14 18:37:27 -04:00
if task . RestartPolicy == nil {
task . RestartPolicy = & swarm . RestartPolicy { }
}
if flags . Changed ( flagRestartCondition ) {
value , _ := flags . GetString ( flagRestartCondition )
task . RestartPolicy . Condition = swarm . RestartPolicyCondition ( value )
}
2016-07-26 02:53:20 -04:00
updateDurationOpt ( flagRestartDelay , & task . RestartPolicy . Delay )
updateUint64Opt ( flagRestartMaxAttempts , & task . RestartPolicy . MaxAttempts )
updateDurationOpt ( flagRestartWindow , & task . RestartPolicy . Window )
2016-06-14 18:37:27 -04:00
}
2016-06-29 12:28:33 -04:00
if anyChanged ( flags , flagConstraintAdd , flagConstraintRemove ) {
2016-06-24 11:53:49 -04:00
if task . Placement == nil {
task . Placement = & swarm . Placement { }
}
updatePlacement ( flags , task . Placement )
2016-06-20 12:57:57 -04:00
}
2016-06-13 22:56:23 -04:00
2016-06-17 14:14:36 -04:00
if err := updateReplicas ( flags , & spec . Mode ) ; err != nil {
2016-06-13 22:56:23 -04:00
return err
}
2016-07-22 14:35:51 -04:00
if anyChanged ( flags , flagUpdateParallelism , flagUpdateDelay , flagUpdateFailureAction ) {
2016-06-14 18:37:27 -04:00
if spec . UpdateConfig == nil {
spec . UpdateConfig = & swarm . UpdateConfig { }
}
2016-06-17 19:24:07 -04:00
updateUint64 ( flagUpdateParallelism , & spec . UpdateConfig . Parallelism )
updateDuration ( flagUpdateDelay , & spec . UpdateConfig . Delay )
2016-07-22 14:35:51 -04:00
updateString ( flagUpdateFailureAction , & spec . UpdateConfig . FailureAction )
2016-06-14 18:37:27 -04:00
}
2016-06-13 22:56:23 -04:00
2016-06-14 12:33:50 -04:00
if flags . Changed ( flagEndpointMode ) {
value , _ := flags . GetString ( flagEndpointMode )
2016-07-26 02:53:20 -04:00
if spec . EndpointSpec == nil {
spec . EndpointSpec = & swarm . EndpointSpec { }
}
2016-06-13 22:56:23 -04:00
spec . EndpointSpec . Mode = swarm . ResolutionMode ( value )
}
2016-06-29 12:28:33 -04:00
if anyChanged ( flags , flagPublishAdd , flagPublishRemove ) {
2016-06-14 18:37:27 -04:00
if spec . EndpointSpec == nil {
spec . EndpointSpec = & swarm . EndpointSpec { }
}
2016-08-04 23:51:28 -04:00
if err := updatePorts ( flags , & spec . EndpointSpec . Ports ) ; err != nil {
return err
}
2016-06-14 18:37:27 -04:00
}
2016-07-08 21:44:18 -04:00
if err := updateLogDriver ( flags , & spec . TaskTemplate ) ; err != nil {
return err
}
2016-06-13 22:56:23 -04:00
return nil
}
2016-06-24 15:03:26 -04:00
func updateStringToSlice ( flags * pflag . FlagSet , flag string , field * [ ] string ) error {
if ! flags . Changed ( flag ) {
return nil
}
value , _ := flags . GetString ( flag )
valueSlice , err := shlex . Split ( value )
* field = valueSlice
return err
}
2016-06-20 12:57:57 -04:00
func anyChanged ( flags * pflag . FlagSet , fields ... string ) bool {
for _ , flag := range fields {
if flags . Changed ( flag ) {
return true
}
2016-06-13 22:56:23 -04:00
}
2016-06-20 12:57:57 -04:00
return false
}
2016-06-24 11:53:49 -04:00
func updatePlacement ( flags * pflag . FlagSet , placement * swarm . Placement ) {
2016-06-29 12:28:33 -04:00
field , _ := flags . GetStringSlice ( flagConstraintAdd )
2016-06-24 11:53:49 -04:00
placement . Constraints = append ( placement . Constraints , field ... )
toRemove := buildToRemoveSet ( flags , flagConstraintRemove )
2016-07-13 15:59:23 -04:00
placement . Constraints = removeItems ( placement . Constraints , toRemove , itemKey )
2016-06-24 11:53:49 -04:00
}
2016-07-25 05:38:01 -04:00
func updateContainerLabels ( flags * pflag . FlagSet , field * map [ string ] string ) {
if flags . Changed ( flagContainerLabelAdd ) {
2016-07-25 22:17:06 -04:00
if * field == nil {
2016-07-25 05:38:01 -04:00
* field = map [ string ] string { }
}
values := flags . Lookup ( flagContainerLabelAdd ) . Value . ( * opts . ListOpts ) . GetAll ( )
for key , value := range runconfigopts . ConvertKVStringsToMap ( values ) {
( * field ) [ key ] = value
}
}
2016-07-25 22:17:06 -04:00
if * field != nil && flags . Changed ( flagContainerLabelRemove ) {
2016-07-25 05:38:01 -04:00
toRemove := flags . Lookup ( flagContainerLabelRemove ) . Value . ( * opts . ListOpts ) . GetAll ( )
for _ , label := range toRemove {
delete ( * field , label )
}
}
}
2016-06-20 12:57:57 -04:00
func updateLabels ( flags * pflag . FlagSet , field * map [ string ] string ) {
2016-06-29 12:28:33 -04:00
if flags . Changed ( flagLabelAdd ) {
2016-07-25 15:54:38 -04:00
if * field == nil {
2016-06-20 12:57:57 -04:00
* field = map [ string ] string { }
}
2016-06-13 22:56:23 -04:00
2016-06-29 12:28:33 -04:00
values := flags . Lookup ( flagLabelAdd ) . Value . ( * opts . ListOpts ) . GetAll ( )
2016-06-20 12:57:57 -04:00
for key , value := range runconfigopts . ConvertKVStringsToMap ( values ) {
( * field ) [ key ] = value
}
}
2016-06-17 19:24:07 -04:00
2016-07-25 15:54:38 -04:00
if * field != nil && flags . Changed ( flagLabelRemove ) {
2016-06-29 12:28:33 -04:00
toRemove := flags . Lookup ( flagLabelRemove ) . Value . ( * opts . ListOpts ) . GetAll ( )
2016-06-20 12:57:57 -04:00
for _ , label := range toRemove {
delete ( * field , label )
}
2016-06-13 22:56:23 -04:00
}
}
2016-06-20 12:57:57 -04:00
func updateEnvironment ( flags * pflag . FlagSet , field * [ ] string ) {
2016-08-04 23:08:22 -04:00
envSet := map [ string ] string { }
for _ , v := range * field {
envSet [ envKey ( v ) ] = v
}
2016-06-29 12:28:33 -04:00
if flags . Changed ( flagEnvAdd ) {
value := flags . Lookup ( flagEnvAdd ) . Value . ( * opts . ListOpts )
2016-08-04 23:08:22 -04:00
for _ , v := range value . GetAll ( ) {
envSet [ envKey ( v ) ] = v
}
2016-06-20 12:57:57 -04:00
}
2016-08-04 23:08:22 -04:00
* field = [ ] string { }
for _ , v := range envSet {
* field = append ( * field , v )
}
2016-06-24 11:53:49 -04:00
toRemove := buildToRemoveSet ( flags , flagEnvRemove )
2016-07-13 15:59:23 -04:00
* field = removeItems ( * field , toRemove , envKey )
2016-06-17 11:01:46 -04:00
}
2016-06-20 12:57:57 -04:00
func envKey ( value string ) string {
kv := strings . SplitN ( value , "=" , 2 )
return kv [ 0 ]
}
2016-07-13 15:59:23 -04:00
func itemKey ( value string ) string {
return value
}
2016-06-24 11:53:49 -04:00
func buildToRemoveSet ( flags * pflag . FlagSet , flag string ) map [ string ] struct { } {
var empty struct { }
toRemove := make ( map [ string ] struct { } )
if ! flags . Changed ( flag ) {
return toRemove
}
2016-06-29 12:28:33 -04:00
toRemoveSlice := flags . Lookup ( flag ) . Value . ( * opts . ListOpts ) . GetAll ( )
2016-06-24 11:53:49 -04:00
for _ , key := range toRemoveSlice {
toRemove [ key ] = empty
}
return toRemove
}
2016-07-13 15:59:23 -04:00
func removeItems (
seq [ ] string ,
toRemove map [ string ] struct { } ,
keyFunc func ( string ) string ,
) [ ] string {
newSeq := [ ] string { }
for _ , item := range seq {
if _ , exists := toRemove [ keyFunc ( item ) ] ; ! exists {
newSeq = append ( newSeq , item )
}
}
return newSeq
}
2016-08-15 12:13:18 -04:00
func updateMounts ( flags * pflag . FlagSet , mounts * [ ] mounttypes . Mount ) {
2016-06-29 12:28:33 -04:00
if flags . Changed ( flagMountAdd ) {
values := flags . Lookup ( flagMountAdd ) . Value . ( * MountOpt ) . Value ( )
2016-06-20 12:57:57 -04:00
* mounts = append ( * mounts , values ... )
}
2016-06-24 11:53:49 -04:00
toRemove := buildToRemoveSet ( flags , flagMountRemove )
2016-07-13 15:59:23 -04:00
2016-08-15 12:13:18 -04:00
newMounts := [ ] mounttypes . Mount { }
2016-07-13 15:59:23 -04:00
for _ , mount := range * mounts {
if _ , exists := toRemove [ mount . Target ] ; ! exists {
newMounts = append ( newMounts , mount )
2016-06-20 12:57:57 -04:00
}
2016-06-13 22:56:23 -04:00
}
2016-07-13 15:59:23 -04:00
* mounts = newMounts
2016-06-13 22:56:23 -04:00
}
2016-08-04 23:51:28 -04:00
type byPortConfig [ ] swarm . PortConfig
func ( r byPortConfig ) Len ( ) int { return len ( r ) }
func ( r byPortConfig ) Swap ( i , j int ) { r [ i ] , r [ j ] = r [ j ] , r [ i ] }
func ( r byPortConfig ) Less ( i , j int ) bool {
// We convert PortConfig into `port/protocol`, e.g., `80/tcp`
// In updatePorts we already filter out with map so there is duplicate entries
return portConfigToString ( & r [ i ] ) < portConfigToString ( & r [ j ] )
}
func portConfigToString ( portConfig * swarm . PortConfig ) string {
protocol := portConfig . Protocol
if protocol == "" {
protocol = "tcp"
}
return fmt . Sprintf ( "%v/%s" , portConfig . PublishedPort , protocol )
}
func updatePorts ( flags * pflag . FlagSet , portConfig * [ ] swarm . PortConfig ) error {
// The key of the map is `port/protocol`, e.g., `80/tcp`
portSet := map [ string ] swarm . PortConfig { }
// Check to see if there are any conflict in flags.
2016-06-29 12:28:33 -04:00
if flags . Changed ( flagPublishAdd ) {
values := flags . Lookup ( flagPublishAdd ) . Value . ( * opts . ListOpts ) . GetAll ( )
2016-06-20 12:57:57 -04:00
ports , portBindings , _ := nat . ParsePortSpecs ( values )
2016-06-13 22:56:23 -04:00
2016-06-20 12:57:57 -04:00
for port := range ports {
2016-08-04 23:51:28 -04:00
newConfigs := convertPortToPortConfig ( port , portBindings )
for _ , entry := range newConfigs {
if v , ok := portSet [ portConfigToString ( & entry ) ] ; ok && v != entry {
return fmt . Errorf ( "conflicting port mapping between %v:%v/%s and %v:%v/%s" , entry . PublishedPort , entry . TargetPort , entry . Protocol , v . PublishedPort , v . TargetPort , v . Protocol )
}
portSet [ portConfigToString ( & entry ) ] = entry
}
2016-06-20 12:57:57 -04:00
}
}
2016-06-13 22:56:23 -04:00
2016-08-04 23:51:28 -04:00
// Override previous PortConfig in service if there is any duplicate
for _ , entry := range * portConfig {
if _ , ok := portSet [ portConfigToString ( & entry ) ] ; ! ok {
portSet [ portConfigToString ( & entry ) ] = entry
}
2016-07-13 15:59:23 -04:00
}
2016-08-04 23:51:28 -04:00
2016-07-13 15:59:23 -04:00
toRemove := flags . Lookup ( flagPublishRemove ) . Value . ( * opts . ListOpts ) . GetAll ( )
newPorts := [ ] swarm . PortConfig { }
portLoop :
2016-08-04 23:51:28 -04:00
for _ , port := range portSet {
2016-06-20 12:57:57 -04:00
for _ , rawTargetPort := range toRemove {
targetPort := nat . Port ( rawTargetPort )
2016-07-13 15:59:23 -04:00
if equalPort ( targetPort , port ) {
continue portLoop
2016-06-20 12:57:57 -04:00
}
}
2016-07-13 15:59:23 -04:00
newPorts = append ( newPorts , port )
2016-06-13 22:56:23 -04:00
}
2016-08-04 23:51:28 -04:00
// Sort the PortConfig to avoid unnecessary updates
sort . Sort ( byPortConfig ( newPorts ) )
2016-07-13 15:59:23 -04:00
* portConfig = newPorts
2016-08-04 23:51:28 -04:00
return nil
2016-07-13 15:59:23 -04:00
}
func equalPort ( targetPort nat . Port , port swarm . PortConfig ) bool {
return ( string ( port . Protocol ) == targetPort . Proto ( ) &&
port . TargetPort == uint32 ( targetPort . Int ( ) ) )
2016-06-13 22:56:23 -04:00
}
2016-06-17 14:14:36 -04:00
func updateReplicas ( flags * pflag . FlagSet , serviceMode * swarm . ServiceMode ) error {
if ! flags . Changed ( flagReplicas ) {
2016-06-13 22:56:23 -04:00
return nil
}
2016-07-26 02:53:20 -04:00
if serviceMode == nil || serviceMode . Replicated == nil {
2016-06-13 22:56:23 -04:00
return fmt . Errorf ( "replicas can only be used with replicated mode" )
}
2016-06-17 14:14:36 -04:00
serviceMode . Replicated . Replicas = flags . Lookup ( flagReplicas ) . Value . ( * Uint64Opt ) . Value ( )
2016-06-13 22:56:23 -04:00
return nil
}
2016-07-08 21:44:18 -04:00
// updateLogDriver updates the log driver only if the log driver flag is set.
// All options will be replaced with those provided on the command line.
func updateLogDriver ( flags * pflag . FlagSet , taskTemplate * swarm . TaskSpec ) error {
if ! flags . Changed ( flagLogDriver ) {
return nil
}
name , err := flags . GetString ( flagLogDriver )
if err != nil {
return err
}
if name == "" {
return nil
}
taskTemplate . LogDriver = & swarm . Driver {
Name : name ,
Options : runconfigopts . ConvertKVStringsToMap ( flags . Lookup ( flagLogOpt ) . Value . ( * opts . ListOpts ) . GetAll ( ) ) ,
}
return nil
}