mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
feat(jobs): add the ability to run a job on a target endpoint #2374
* feat(jobs): adding the ability to run scripts on endpoints fix(job): click on containerId in JobsDatatable redirects to container's logs refactor(job): remove the jobs datatable settings + texts changes on JobCreation view fix(jobs): jobs payloads are now following API rules and case feat(jobs): adding the capability to run scripts on hosts * feat(jobs): adding the ability to purge jobs containers * refactor(job): apply review changes * feat(job-creation): store image name in local storage * feat(host): disable job exec link in non-agent Swarm setup * feat(host): only display execute job in agent setups or standalone * feat(job): job execution overhaul * docs(swagger): update EndpointJob documentation
This commit is contained in:
parent
6ab510e5cb
commit
354fda31f1
37 changed files with 739 additions and 100 deletions
|
@ -26,12 +26,13 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService) *Clien
|
|||
}
|
||||
|
||||
// CreateClient is a generic function to create a Docker client based on
|
||||
// a specific endpoint configuration
|
||||
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||
// a specific endpoint configuration. The nodeName parameter can be used
|
||||
// with an agent enabled endpoint to target a specific node in an agent cluster.
|
||||
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string) (*client.Client, error) {
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
return nil, unsupportedEnvironmentType
|
||||
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
return createAgentClient(endpoint, factory.signatureService)
|
||||
return createAgentClient(endpoint, factory.signatureService, nodeName)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
|
||||
|
@ -60,7 +61,7 @@ func createTCPClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
|||
)
|
||||
}
|
||||
|
||||
func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService) (*client.Client, error) {
|
||||
func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, nodeName string) (*client.Client, error) {
|
||||
httpCli, err := httpClient(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -76,6 +77,10 @@ func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.
|
|||
portainer.PortainerAgentSignatureHeader: signature,
|
||||
}
|
||||
|
||||
if nodeName != "" {
|
||||
headers[portainer.PortainerAgentTargetHeader] = nodeName
|
||||
}
|
||||
|
||||
return client.NewClientWithOpts(
|
||||
client.WithHost(endpoint.URL),
|
||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
||||
|
|
|
@ -29,13 +29,13 @@ func NewJobService(dockerClientFactory *ClientFactory) *JobService {
|
|||
}
|
||||
|
||||
// Execute will execute a script on the endpoint host with the supplied image as a container
|
||||
func (service *JobService) Execute(endpoint *portainer.Endpoint, image string, script []byte) error {
|
||||
func (service *JobService) Execute(endpoint *portainer.Endpoint, nodeName, image string, script []byte) error {
|
||||
buffer, err := archive.TarFileInBuffer(script, "script.sh", 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cli, err := service.DockerClientFactory.CreateClient(endpoint)
|
||||
cli, err := service.DockerClientFactory.CreateClient(endpoint, nodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ func NewSnapshotter(clientFactory *ClientFactory) *Snapshotter {
|
|||
|
||||
// CreateSnapshot creates a snapshot of a specific endpoint
|
||||
func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*portainer.Snapshot, error) {
|
||||
cli, err := snapshotter.clientFactory.CreateClient(endpoint)
|
||||
cli, err := snapshotter.clientFactory.CreateClient(endpoint, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ func (payload *endpointJobFromFileContentPayload) Validate(r *http.Request) erro
|
|||
return nil
|
||||
}
|
||||
|
||||
// POST request on /api/endpoints/:id/job?method
|
||||
// POST request on /api/endpoints/:id/job?method&nodeName
|
||||
func (handler *Handler) endpointJob(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
|
@ -61,6 +61,8 @@ func (handler *Handler) endpointJob(w http.ResponseWriter, r *http.Request) *htt
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method", err}
|
||||
}
|
||||
|
||||
nodeName, _ := request.RetrieveQueryParameter(r, "nodeName", true)
|
||||
|
||||
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}
|
||||
|
@ -75,22 +77,22 @@ func (handler *Handler) endpointJob(w http.ResponseWriter, r *http.Request) *htt
|
|||
|
||||
switch method {
|
||||
case "file":
|
||||
return handler.executeJobFromFile(w, r, endpoint)
|
||||
return handler.executeJobFromFile(w, r, endpoint, nodeName)
|
||||
case "string":
|
||||
return handler.executeJobFromFileContent(w, r, endpoint)
|
||||
return handler.executeJobFromFileContent(w, r, endpoint, nodeName)
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string or file", errors.New(request.ErrInvalidQueryParameter)}
|
||||
}
|
||||
|
||||
func (handler *Handler) executeJobFromFile(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
func (handler *Handler) executeJobFromFile(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, nodeName string) *httperror.HandlerError {
|
||||
payload := &endpointJobFromFilePayload{}
|
||||
err := payload.Validate(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
err = handler.JobService.Execute(endpoint, payload.Image, payload.File)
|
||||
err = handler.JobService.Execute(endpoint, nodeName, payload.Image, payload.File)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed executing job", err}
|
||||
}
|
||||
|
@ -98,14 +100,14 @@ func (handler *Handler) executeJobFromFile(w http.ResponseWriter, r *http.Reques
|
|||
return response.Empty(w)
|
||||
}
|
||||
|
||||
func (handler *Handler) executeJobFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
func (handler *Handler) executeJobFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, nodeName string) *httperror.HandlerError {
|
||||
var payload endpointJobFromFileContentPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
err = handler.JobService.Execute(endpoint, payload.Image, []byte(payload.FileContent))
|
||||
err = handler.JobService.Execute(endpoint, nodeName, payload.Image, []byte(payload.FileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed executing job", err}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ func (handler *Handler) webhookExecute(w http.ResponseWriter, r *http.Request) *
|
|||
}
|
||||
|
||||
func (handler *Handler) executeServiceWebhook(w http.ResponseWriter, endpoint *portainer.Endpoint, resourceID string) *httperror.HandlerError {
|
||||
dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint)
|
||||
dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint, "")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Error creating docker client", err}
|
||||
}
|
||||
|
|
|
@ -638,7 +638,7 @@ type (
|
|||
|
||||
// JobService represents a service to manage job execution on hosts
|
||||
JobService interface {
|
||||
Execute(endpoint *Endpoint, image string, script []byte) error
|
||||
Execute(endpoint *Endpoint, nodeName, image string, script []byte) error
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -537,6 +537,11 @@ paths:
|
|||
description: "Job execution method. Possible values: file or string."
|
||||
required: true
|
||||
type: "string"
|
||||
- name: "nodeName"
|
||||
in: "query"
|
||||
description: "Optional. Hostname of a node when targeting a Portainer agent cluster."
|
||||
required: true
|
||||
type: "string"
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Job details. Required when method equals string."
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue