diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index a6a921acf..192bc0271 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -21,6 +21,7 @@ import ( "github.com/portainer/portainer/bolt/template" "github.com/portainer/portainer/bolt/user" "github.com/portainer/portainer/bolt/version" + "github.com/portainer/portainer/bolt/webhook" ) const ( @@ -47,6 +48,7 @@ type Store struct { TemplateService *template.Service UserService *user.Service VersionService *version.Service + WebhookService *webhook.Service } // NewStore initializes a new Store and the associated services @@ -232,5 +234,11 @@ func (store *Store) initServices() error { } store.VersionService = versionService + webhookService, err := webhook.NewService(store.db) + if err != nil { + return err + } + store.WebhookService = webhookService + return nil } diff --git a/api/bolt/webhook/webhook.go b/api/bolt/webhook/webhook.go new file mode 100644 index 000000000..94ebe61c5 --- /dev/null +++ b/api/bolt/webhook/webhook.go @@ -0,0 +1,151 @@ +package webhook + +import ( + "github.com/portainer/portainer" + "github.com/portainer/portainer/bolt/internal" + + "github.com/boltdb/bolt" +) + +const ( + // BucketName represents the name of the bucket where this service stores data. + BucketName = "webhooks" +) + +// Service represents a service for managing webhook data. +type Service struct { + db *bolt.DB +} + +// NewService creates a new instance of a service. +func NewService(db *bolt.DB) (*Service, error) { + err := internal.CreateBucket(db, BucketName) + if err != nil { + return nil, err + } + + return &Service{ + db: db, + }, nil +} + +//Webhooks returns an array of all webhooks +func (service *Service) Webhooks() ([]portainer.Webhook, error) { + var webhooks = make([]portainer.Webhook, 0) + + err := service.db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketName)) + + cursor := bucket.Cursor() + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + var webhook portainer.Webhook + err := internal.UnmarshalObject(v, &webhook) + if err != nil { + return err + } + webhooks = append(webhooks, webhook) + } + + return nil + }) + + return webhooks, err +} + +// Webhook returns a webhook by ID. +func (service *Service) Webhook(ID portainer.WebhookID) (*portainer.Webhook, error) { + var webhook portainer.Webhook + identifier := internal.Itob(int(ID)) + + err := internal.GetObject(service.db, BucketName, identifier, &webhook) + if err != nil { + return nil, err + } + + return &webhook, nil +} + +// WebhookByResourceID returns a webhook by the ResourceID it is associated with. +func (service *Service) WebhookByResourceID(ID string) (*portainer.Webhook, error) { + var webhook *portainer.Webhook + + err := service.db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketName)) + cursor := bucket.Cursor() + + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + var w portainer.Webhook + err := internal.UnmarshalObject(v, &w) + if err != nil { + return err + } + + if w.ResourceID == ID { + webhook = &w + break + } + } + + if webhook == nil { + return portainer.ErrObjectNotFound + } + + return nil + }) + + return webhook, err +} + +// WebhookByToken returns a webhook by the random token it is associated with. +func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error) { + var webhook *portainer.Webhook + + err := service.db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketName)) + cursor := bucket.Cursor() + + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + var w portainer.Webhook + err := internal.UnmarshalObject(v, &w) + if err != nil { + return err + } + + if w.Token == token { + webhook = &w + break + } + } + + if webhook == nil { + return portainer.ErrObjectNotFound + } + + return nil + }) + + return webhook, err +} + +// DeleteWebhook deletes a webhook. +func (service *Service) DeleteWebhook(ID portainer.WebhookID) error { + identifier := internal.Itob(int(ID)) + return internal.DeleteObject(service.db, BucketName, identifier) +} + +// CreateWebhook assign an ID to a new webhook and saves it. +func (service *Service) CreateWebhook(webhook *portainer.Webhook) error { + return service.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketName)) + + id, _ := bucket.NextSequence() + webhook.ID = portainer.WebhookID(id) + + data, err := internal.MarshalObject(webhook) + if err != nil { + return err + } + + return bucket.Put(internal.Itob(int(webhook.ID)), data) + }) +} diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index c7997bf14..cecf986b7 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -505,6 +505,7 @@ func main() { StackService: store.StackService, TagService: store.TagService, TemplateService: store.TemplateService, + WebhookService: store.WebhookService, SwarmStackManager: swarmStackManager, ComposeStackManager: composeStackManager, CryptoService: cryptoService, @@ -518,6 +519,7 @@ func main() { SSL: *flags.SSL, SSLCert: *flags.SSLCert, SSLKey: *flags.SSLKey, + DockerClientFactory: clientFactory, } log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr) diff --git a/api/errors.go b/api/errors.go index 37552f104..e348aaf48 100644 --- a/api/errors.go +++ b/api/errors.go @@ -93,3 +93,9 @@ type Error string // Error returns the error message. func (e Error) Error() string { return string(e) } + +// Webhook errors +const ( + ErrWebhookAlreadyExists = Error("A webhook for this resource already exists") + ErrUnsupportedWebhookType = Error("Webhooks for this resource are not currently supported") +) diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 342230396..40ae9f57d 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -22,6 +22,7 @@ import ( "github.com/portainer/portainer/http/handler/templates" "github.com/portainer/portainer/http/handler/upload" "github.com/portainer/portainer/http/handler/users" + "github.com/portainer/portainer/http/handler/webhooks" "github.com/portainer/portainer/http/handler/websocket" ) @@ -47,6 +48,7 @@ type Handler struct { UploadHandler *upload.Handler UserHandler *users.Handler WebSocketHandler *websocket.Handler + WebhookHandler *webhooks.Handler } // ServeHTTP delegates a request to the appropriate subhandler. @@ -95,6 +97,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.StripPrefix("/api", h.TeamMembershipHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/websocket"): http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r) + case strings.HasPrefix(r.URL.Path, "/api/webhooks"): + http.StripPrefix("/api", h.WebhookHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/"): h.FileHandler.ServeHTTP(w, r) } diff --git a/api/http/handler/webhooks/handler.go b/api/http/handler/webhooks/handler.go new file mode 100644 index 000000000..fe60f3fdb --- /dev/null +++ b/api/http/handler/webhooks/handler.go @@ -0,0 +1,35 @@ +package webhooks + +import ( + "net/http" + + "github.com/gorilla/mux" + portainer "github.com/portainer/portainer" + "github.com/portainer/portainer/docker" + httperror "github.com/portainer/portainer/http/error" + "github.com/portainer/portainer/http/security" +) + +// Handler is the HTTP handler used to handle webhook operations. +type Handler struct { + *mux.Router + WebhookService portainer.WebhookService + EndpointService portainer.EndpointService + DockerClientFactory *docker.ClientFactory +} + +// NewHandler creates a handler to manage settings operations. +func NewHandler(bouncer *security.RequestBouncer) *Handler { + h := &Handler{ + Router: mux.NewRouter(), + } + h.Handle("/webhooks", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookCreate))).Methods(http.MethodPost) + h.Handle("/webhooks", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookList))).Methods(http.MethodGet) + h.Handle("/webhooks/{id}", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookDelete))).Methods(http.MethodDelete) + h.Handle("/webhooks/{token}", + bouncer.PublicAccess(httperror.LoggerHandler(h.webhookExecute))).Methods(http.MethodPost) + return h +} diff --git a/api/http/handler/webhooks/webhook_create.go b/api/http/handler/webhooks/webhook_create.go new file mode 100644 index 000000000..c74db0c6e --- /dev/null +++ b/api/http/handler/webhooks/webhook_create.go @@ -0,0 +1,66 @@ +package webhooks + +import ( + "net/http" + + "github.com/asaskevich/govalidator" + "github.com/portainer/portainer" + httperror "github.com/portainer/portainer/http/error" + "github.com/portainer/portainer/http/request" + "github.com/portainer/portainer/http/response" + "github.com/satori/go.uuid" +) + +type webhookCreatePayload struct { + ResourceID string + EndpointID int + WebhookType int +} + +func (payload *webhookCreatePayload) Validate(r *http.Request) error { + if govalidator.IsNull(payload.ResourceID) { + return portainer.Error("Invalid ResourceID") + } + if payload.EndpointID == 0 { + return portainer.Error("Invalid EndpointID") + } + if payload.WebhookType != 1 { + return portainer.Error("Invalid WebhookType") + } + return nil +} + +func (handler *Handler) webhookCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + var payload webhookCreatePayload + err := request.DecodeAndValidateJSONPayload(r, &payload) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} + } + + webhook, err := handler.WebhookService.WebhookByResourceID(payload.ResourceID) + if err != nil && err != portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusInternalServerError, "An error occurred retrieving webhooks from the database", err} + } + if webhook != nil { + return &httperror.HandlerError{http.StatusConflict, "A webhook for this resource already exists", portainer.ErrWebhookAlreadyExists} + } + + token, err := uuid.NewV4() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Error creating unique token", err} + } + + webhook = &portainer.Webhook{ + Token: token.String(), + ResourceID: payload.ResourceID, + EndpointID: portainer.EndpointID(payload.EndpointID), + WebhookType: portainer.WebhookType(payload.WebhookType), + } + + err = handler.WebhookService.CreateWebhook(webhook) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the webhook inside the database", err} + } + + return response.JSON(w, webhook) +} diff --git a/api/http/handler/webhooks/webhook_delete.go b/api/http/handler/webhooks/webhook_delete.go new file mode 100644 index 000000000..b81a445e1 --- /dev/null +++ b/api/http/handler/webhooks/webhook_delete.go @@ -0,0 +1,25 @@ +package webhooks + +import ( + "net/http" + + "github.com/portainer/portainer" + httperror "github.com/portainer/portainer/http/error" + "github.com/portainer/portainer/http/request" + "github.com/portainer/portainer/http/response" +) + +// DELETE request on /api/webhook/:serviceID +func (handler *Handler) webhookDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + id, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid webhook id", err} + } + + err = handler.WebhookService.DeleteWebhook(portainer.WebhookID(id)) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the webhook from the database", err} + } + + return response.Empty(w) +} diff --git a/api/http/handler/webhooks/webhook_execute.go b/api/http/handler/webhooks/webhook_execute.go new file mode 100644 index 000000000..395be9999 --- /dev/null +++ b/api/http/handler/webhooks/webhook_execute.go @@ -0,0 +1,71 @@ +package webhooks + +import ( + "context" + "net/http" + "strings" + + dockertypes "github.com/docker/docker/api/types" + "github.com/portainer/portainer" + httperror "github.com/portainer/portainer/http/error" + "github.com/portainer/portainer/http/request" + "github.com/portainer/portainer/http/response" +) + +// Acts on a passed in token UUID to restart the docker service +func (handler *Handler) webhookExecute(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + + webhookToken, err := request.RetrieveRouteVariableValue(r, "token") + + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Invalid service id parameter", err} + } + + webhook, err := handler.WebhookService.WebhookByToken(webhookToken) + + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find a webhook with this token", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve webhook from the database", err} + } + + resourceID := webhook.ResourceID + endpointID := webhook.EndpointID + webhookType := webhook.WebhookType + + endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + } + switch webhookType { + case portainer.ServiceWebhook: + return handler.executeServiceWebhook(w, endpoint, resourceID) + default: + return &httperror.HandlerError{http.StatusInternalServerError, "Unsupported webhook type", portainer.ErrUnsupportedWebhookType} + } + +} + +func (handler *Handler) executeServiceWebhook(w http.ResponseWriter, endpoint *portainer.Endpoint, resourceID string) *httperror.HandlerError { + dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Error creating docker client", err} + } + defer dockerClient.Close() + + service, _, err := dockerClient.ServiceInspectWithRaw(context.Background(), resourceID, dockertypes.ServiceInspectOptions{InsertDefaults: true}) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Error looking up service", err} + } + + service.Spec.TaskTemplate.ForceUpdate++ + + service.Spec.TaskTemplate.ContainerSpec.Image = strings.Split(service.Spec.TaskTemplate.ContainerSpec.Image, "@sha")[0] + _, err = dockerClient.ServiceUpdate(context.Background(), resourceID, service.Version, service.Spec, dockertypes.ServiceUpdateOptions{QueryRegistry: true}) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Error updating service", err} + } + return response.Empty(w) +} diff --git a/api/http/handler/webhooks/webhook_list.go b/api/http/handler/webhooks/webhook_list.go new file mode 100644 index 000000000..288bdcd65 --- /dev/null +++ b/api/http/handler/webhooks/webhook_list.go @@ -0,0 +1,47 @@ +package webhooks + +import ( + "net/http" + + "github.com/portainer/portainer" + httperror "github.com/portainer/portainer/http/error" + "github.com/portainer/portainer/http/request" + "github.com/portainer/portainer/http/response" +) + +type webhookListOperationFilters struct { + ResourceID string `json:"ResourceID"` + EndpointID int `json:"EndpointID"` +} + +// GET request on /api/webhooks?(filters=) +func (handler *Handler) webhookList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + var filters webhookListOperationFilters + err := request.RetrieveJSONQueryParameter(r, "filters", &filters, true) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: filters", err} + } + + webhooks, err := handler.WebhookService.Webhooks() + webhooks = filterWebhooks(webhooks, &filters) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve webhooks from the database", err} + } + + return response.JSON(w, webhooks) +} + +func filterWebhooks(webhooks []portainer.Webhook, filters *webhookListOperationFilters) []portainer.Webhook { + if filters.EndpointID == 0 && filters.ResourceID == "" { + return webhooks + } + + filteredWebhooks := make([]portainer.Webhook, 0, len(webhooks)) + for _, webhook := range webhooks { + if webhook.EndpointID == portainer.EndpointID(filters.EndpointID) && webhook.ResourceID == string(filters.ResourceID) { + filteredWebhooks = append(filteredWebhooks, webhook) + } + } + + return filteredWebhooks +} diff --git a/api/http/server.go b/api/http/server.go index 116a6c1ec..b9f1f2bb7 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -4,6 +4,7 @@ import ( "time" "github.com/portainer/portainer" + "github.com/portainer/portainer/docker" "github.com/portainer/portainer/http/handler" "github.com/portainer/portainer/http/handler/auth" "github.com/portainer/portainer/http/handler/dockerhub" @@ -23,6 +24,7 @@ import ( "github.com/portainer/portainer/http/handler/templates" "github.com/portainer/portainer/http/handler/upload" "github.com/portainer/portainer/http/handler/users" + "github.com/portainer/portainer/http/handler/webhooks" "github.com/portainer/portainer/http/handler/websocket" "github.com/portainer/portainer/http/proxy" "github.com/portainer/portainer/http/security" @@ -60,10 +62,12 @@ type Server struct { TeamMembershipService portainer.TeamMembershipService TemplateService portainer.TemplateService UserService portainer.UserService + WebhookService portainer.WebhookService Handler *handler.Handler SSL bool SSLCert string SSLKey string + DockerClientFactory *docker.ClientFactory } // Start starts the HTTP server @@ -171,6 +175,11 @@ func (server *Server) Start() error { websocketHandler.EndpointService = server.EndpointService websocketHandler.SignatureService = server.SignatureService + var webhookHandler = webhooks.NewHandler(requestBouncer) + webhookHandler.WebhookService = server.WebhookService + webhookHandler.EndpointService = server.EndpointService + webhookHandler.DockerClientFactory = server.DockerClientFactory + server.Handler = &handler.Handler{ AuthHandler: authHandler, DockerHubHandler: dockerHubHandler, @@ -191,6 +200,7 @@ func (server *Server) Start() error { UploadHandler: uploadHandler, UserHandler: userHandler, WebSocketHandler: websocketHandler, + WebhookHandler: webhookHandler, } if server.SSL { diff --git a/api/portainer.go b/api/portainer.go index 49f87c652..839abea7a 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -220,6 +220,21 @@ type ( TLSKeyPath string `json:"TLSKey,omitempty"` } + // WebhookID represents an webhook identifier. + WebhookID int + + // WebhookType represents the type of resource a webhook is related to + WebhookType int + + // Webhook represents a url webhook that can be used to update a service + Webhook struct { + ID WebhookID `json:"Id"` + Token string `json:"Token"` + ResourceID string `json:"ResourceId"` + EndpointID EndpointID `json:"EndpointId"` + WebhookType WebhookType `json:"Type"` + } + // AzureCredentials represents the credentials used to connect to an Azure // environment. AzureCredentials struct { @@ -506,6 +521,16 @@ type ( StoreDBVersion(version int) error } + // WebhookService represents a service for managing webhook data. + WebhookService interface { + Webhooks() ([]Webhook, error) + Webhook(ID WebhookID) (*Webhook, error) + CreateWebhook(portainer *Webhook) error + WebhookByResourceID(resourceID string) (*Webhook, error) + WebhookByToken(token string) (*Webhook, error) + DeleteWebhook(serviceID WebhookID) error + } + // ResourceControlService represents a service for managing resource control data ResourceControlService interface { ResourceControl(ID ResourceControlID) (*ResourceControl, error) @@ -732,3 +757,9 @@ const ( // EndpointStatusDown is used to represent an unavailable endpoint EndpointStatusDown ) + +const ( + _ WebhookType = iota + // ServiceWebhook is a webhook for restarting a docker service + ServiceWebhook +) diff --git a/app/constants.js b/app/constants.js index de9631ddd..b20d44649 100644 --- a/app/constants.js +++ b/app/constants.js @@ -14,6 +14,7 @@ angular.module('portainer') .constant('API_ENDPOINT_TEAMS', 'api/teams') .constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships') .constant('API_ENDPOINT_TEMPLATES', 'api/templates') +.constant('API_ENDPOINT_WEBHOOKS', 'api/webhooks') .constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json') .constant('PAGINATION_MAX_ITEMS', 10) .constant('APPLICATION_CACHE_VALIDITY', 3600) diff --git a/app/docker/components/datatables/services-datatable/actions/servicesDatatableActionsController.js b/app/docker/components/datatables/services-datatable/actions/servicesDatatableActionsController.js index dff0d034c..2f5ce7ce4 100644 --- a/app/docker/components/datatables/services-datatable/actions/servicesDatatableActionsController.js +++ b/app/docker/components/datatables/services-datatable/actions/servicesDatatableActionsController.js @@ -1,6 +1,6 @@ angular.module('portainer.docker') -.controller('ServicesDatatableActionsController', ['$state', 'ServiceService', 'ServiceHelper', 'Notifications', 'ModalService', 'ImageHelper', -function ($state, ServiceService, ServiceHelper, Notifications, ModalService, ImageHelper) { +.controller('ServicesDatatableActionsController', ['$q', '$state', 'ServiceService', 'ServiceHelper', 'Notifications', 'ModalService', 'ImageHelper','WebhookService','EndpointProvider', +function ($q, $state, ServiceService, ServiceHelper, Notifications, ModalService, ImageHelper, WebhookService, EndpointProvider) { this.scaleAction = function scaleService(service) { var config = ServiceHelper.serviceToConfig(service.Model); @@ -71,7 +71,14 @@ function ($state, ServiceService, ServiceHelper, Notifications, ModalService, Im function removeServices(services) { var actionCount = services.length; angular.forEach(services, function (service) { + ServiceService.remove(service) + .then(function success() { + return WebhookService.webhooks(service.Id, EndpointProvider.endpointID()); + }) + .then(function success(data) { + return $q.when(data.length !== 0 && WebhookService.deleteWebhook(data[0].Id)); + }) .then(function success() { Notifications.success('Service successfully removed', service.Name); }) diff --git a/app/docker/views/services/create/createServiceController.js b/app/docker/views/services/create/createServiceController.js index e56dc1e4c..9f69ad0b5 100644 --- a/app/docker/views/services/create/createServiceController.js +++ b/app/docker/views/services/create/createServiceController.js @@ -1,6 +1,6 @@ -angular.module('portainer.docker') -.controller('CreateServiceController', ['$q', '$scope', '$state', '$timeout', 'Service', 'ServiceHelper', 'ConfigService', 'ConfigHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'PluginService', 'RegistryService', 'HttpRequestHelper', 'NodeService', 'SettingsService', -function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, ConfigHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, PluginService, RegistryService, HttpRequestHelper, NodeService, SettingsService) { + angular.module('portainer.docker') +.controller('CreateServiceController', ['$q', '$scope', '$state', '$timeout', 'Service', 'ServiceHelper', 'ConfigService', 'ConfigHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'PluginService', 'RegistryService', 'HttpRequestHelper', 'NodeService', 'SettingsService', 'WebhookService','EndpointProvider', +function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, ConfigHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, PluginService, RegistryService, HttpRequestHelper, NodeService, SettingsService, WebhookService,EndpointProvider) { $scope.formValues = { Name: '', @@ -40,7 +40,8 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C RestartMaxAttempts: 0, RestartWindow: '0s', LogDriverName: '', - LogDriverOpts: [] + LogDriverOpts: [], + Webhook: false }; $scope.state = { @@ -422,9 +423,14 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C var registry = $scope.formValues.Registry; var authenticationDetails = registry.Authentication ? RegistryService.encodedCredentials(registry) : ''; HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails); + + var serviceIdentifier; Service.create(config).$promise .then(function success(data) { - var serviceIdentifier = data.ID; + serviceIdentifier = data.ID; + return $q.when($scope.formValues.Webhook && WebhookService.createServiceWebhook(serviceIdentifier, EndpointProvider.endpointID())); + }) + .then(function success() { var userId = Authentication.getUserDetails().ID; return ResourceControlService.applyResourceControl('service', serviceIdentifier, userId, accessControlData, []); }) diff --git a/app/docker/views/services/create/createservice.html b/app/docker/views/services/create/createservice.html index d9481adb1..40d830f1a 100644 --- a/app/docker/views/services/create/createservice.html +++ b/app/docker/views/services/create/createservice.html @@ -101,6 +101,22 @@ + +
+ Webhooks +
+
+
+ + +
+
+ diff --git a/app/docker/views/services/edit/service.html b/app/docker/views/services/edit/service.html index e0b2b49de..0b3e2e854 100644 --- a/app/docker/views/services/edit/service.html +++ b/app/docker/views/services/edit/service.html @@ -71,6 +71,24 @@ ng-model="service.Image" ng-change="updateServiceAttribute(service, 'Image')" id="image_name" ng-disabled="isUpdating"> + + + Service webhook + + + + + {{ webhookURL | truncatelr }} + + + + + + Service logs @@ -93,7 +111,7 @@