| OLD | NEW |
| 1 // Copyright 2015 The LUCI Authors. | 1 // Copyright 2015 The LUCI Authors. |
| 2 // | 2 // |
| 3 // Licensed under the Apache License, Version 2.0 (the "License"); | 3 // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 // you may not use this file except in compliance with the License. | 4 // you may not use this file except in compliance with the License. |
| 5 // You may obtain a copy of the License at | 5 // You may obtain a copy of the License at |
| 6 // | 6 // |
| 7 // http://www.apache.org/licenses/LICENSE-2.0 | 7 // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 // | 8 // |
| 9 // Unless required by applicable law or agreed to in writing, software | 9 // Unless required by applicable law or agreed to in writing, software |
| 10 // distributed under the License is distributed on an "AS IS" BASIS, | 10 // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 // See the License for the specific language governing permissions and | 12 // See the License for the specific language governing permissions and |
| 13 // limitations under the License. | 13 // limitations under the License. |
| 14 | 14 |
| 15 package ui | 15 package ui |
| 16 | 16 |
| 17 import ( | 17 import ( |
| 18 "crypto/sha1" | 18 "crypto/sha1" |
| 19 "encoding/base64" | 19 "encoding/base64" |
| 20 "fmt" | 20 "fmt" |
| 21 "net/http" | 21 "net/http" |
| 22 "time" | 22 "time" |
| 23 | 23 |
| 24 mc "github.com/luci/gae/service/memcache" | 24 mc "github.com/luci/gae/service/memcache" |
| 25 "github.com/luci/luci-go/common/clock" | 25 "github.com/luci/luci-go/common/clock" |
| 26 » "github.com/luci/luci-go/scheduler/appengine/acl" | 26 » "github.com/luci/luci-go/scheduler/appengine/engine" |
| 27 » "github.com/luci/luci-go/server/auth" | |
| 28 "github.com/luci/luci-go/server/router" | 27 "github.com/luci/luci-go/server/router" |
| 29 "github.com/luci/luci-go/server/templates" | 28 "github.com/luci/luci-go/server/templates" |
| 30 ) | 29 ) |
| 31 | 30 |
| 32 func jobPage(ctx *router.Context) { | 31 func jobPage(ctx *router.Context) { |
| 33 c, w, r, p := ctx.Context, ctx.Writer, ctx.Request, ctx.Params | 32 c, w, r, p := ctx.Context, ctx.Writer, ctx.Request, ctx.Params |
| 34 | 33 |
| 35 projectID := p.ByName("ProjectID") | 34 projectID := p.ByName("ProjectID") |
| 36 jobName := p.ByName("JobName") | 35 jobName := p.ByName("JobName") |
| 37 cursor := r.URL.Query().Get("c") | 36 cursor := r.URL.Query().Get("c") |
| 38 | 37 |
| 39 // Grab the job from the datastore. | 38 // Grab the job from the datastore. |
| 40 » job, err := config(c).Engine.GetJob(c, projectID+"/"+jobName) | 39 » job, err := config(c).Engine.GetVisibleJob(c, projectID+"/"+jobName) |
| 41 if err != nil { | 40 if err != nil { |
| 42 panic(err) | 41 panic(err) |
| 43 } | 42 } |
| 44 if job == nil { | 43 if job == nil { |
| 45 » » http.Error(w, "No such job", http.StatusNotFound) | 44 » » http.Error(w, "No such job or no access to it", http.StatusNotFo
und) |
| 46 return | 45 return |
| 47 } | 46 } |
| 48 | 47 |
| 49 // Grab latest invocations from the datastore. | 48 // Grab latest invocations from the datastore. |
| 50 » invs, nextCursor, err := config(c).Engine.ListInvocations(c, job.JobID,
50, cursor) | 49 » invs, nextCursor, err := config(c).Engine.ListVisibleInvocations(c, job.
JobID, 50, cursor) |
| 51 if err != nil { | 50 if err != nil { |
| 52 panic(err) | 51 panic(err) |
| 53 } | 52 } |
| 54 | 53 |
| 55 // memcacheKey hashes cursor to reduce its length, since full cursor doe
sn't | 54 // memcacheKey hashes cursor to reduce its length, since full cursor doe
sn't |
| 56 // fit into memcache key length limits. Use 'v2' scheme for this ('v1' w
as | 55 // fit into memcache key length limits. Use 'v2' scheme for this ('v1' w
as |
| 57 // used before hashing was added). | 56 // used before hashing was added). |
| 58 memcacheKey := func(cursor string) string { | 57 memcacheKey := func(cursor string) string { |
| 59 blob := sha1.Sum([]byte(job.JobID + ":" + cursor)) | 58 blob := sha1.Sum([]byte(job.JobID + ":" + cursor)) |
| 60 encoded := base64.StdEncoding.EncodeToString(blob[:]) | 59 encoded := base64.StdEncoding.EncodeToString(blob[:]) |
| (...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 96 } | 95 } |
| 97 | 96 |
| 98 //////////////////////////////////////////////////////////////////////////////// | 97 //////////////////////////////////////////////////////////////////////////////// |
| 99 // Actions. | 98 // Actions. |
| 100 | 99 |
| 101 func runJobAction(ctx *router.Context) { | 100 func runJobAction(ctx *router.Context) { |
| 102 c, w, r, p := ctx.Context, ctx.Writer, ctx.Request, ctx.Params | 101 c, w, r, p := ctx.Context, ctx.Writer, ctx.Request, ctx.Params |
| 103 | 102 |
| 104 projectID := p.ByName("ProjectID") | 103 projectID := p.ByName("ProjectID") |
| 105 jobName := p.ByName("JobName") | 104 jobName := p.ByName("JobName") |
| 106 if !acl.IsJobOwner(c, projectID, jobName) { | |
| 107 http.Error(w, "Forbidden", 403) | |
| 108 return | |
| 109 } | |
| 110 | 105 |
| 111 // genericReply renders "we did something (or we failed to do something)
" | 106 // genericReply renders "we did something (or we failed to do something)
" |
| 112 // page, shown on error or if invocation is starting for too long. | 107 // page, shown on error or if invocation is starting for too long. |
| 113 genericReply := func(err error) { | 108 genericReply := func(err error) { |
| 114 templates.MustRender(c, w, "pages/run_job_result.html", map[stri
ng]interface{}{ | 109 templates.MustRender(c, w, "pages/run_job_result.html", map[stri
ng]interface{}{ |
| 115 "ProjectID": projectID, | 110 "ProjectID": projectID, |
| 116 "JobName": jobName, | 111 "JobName": jobName, |
| 117 "Error": err, | 112 "Error": err, |
| 118 }) | 113 }) |
| 119 } | 114 } |
| 120 | 115 |
| 121 // Enqueue new invocation request, and wait for corresponding invocation
to | 116 // Enqueue new invocation request, and wait for corresponding invocation
to |
| 122 // appear. Give up if task queue or datastore indexes are lagging too mu
ch. | 117 // appear. Give up if task queue or datastore indexes are lagging too mu
ch. |
| 123 e := config(c).Engine | 118 e := config(c).Engine |
| 124 jobID := projectID + "/" + jobName | 119 jobID := projectID + "/" + jobName |
| 125 » invNonce, err := e.TriggerInvocation(c, jobID, auth.CurrentIdentity(c)) | 120 » invNonce, err := e.TriggerInvocation(c, jobID) |
| 126 » if err != nil { | 121 » if err == engine.ErrNoOwnerPermission { |
| 122 » » http.Error(w, "Forbidden", 403) |
| 123 » » return |
| 124 » } else if err != nil { |
| 127 genericReply(err) | 125 genericReply(err) |
| 128 return | 126 return |
| 129 } | 127 } |
| 130 | 128 |
| 131 invID := int64(0) | 129 invID := int64(0) |
| 132 deadline := clock.Now(c).Add(10 * time.Second) | 130 deadline := clock.Now(c).Add(10 * time.Second) |
| 133 for invID == 0 && deadline.Sub(clock.Now(c)) > 0 { | 131 for invID == 0 && deadline.Sub(clock.Now(c)) > 0 { |
| 134 // Asking for invocation immediately after triggering it never w
orks, | 132 // Asking for invocation immediately after triggering it never w
orks, |
| 135 // so sleep a bit first. | 133 // so sleep a bit first. |
| 136 if tr := clock.Sleep(c, 600*time.Millisecond); tr.Incomplete() { | 134 if tr := clock.Sleep(c, 600*time.Millisecond); tr.Incomplete() { |
| 137 // The Context was canceled before the Sleep completed.
Terminate the | 135 // The Context was canceled before the Sleep completed.
Terminate the |
| 138 // loop. | 136 // loop. |
| 139 break | 137 break |
| 140 } | 138 } |
| 141 // Find most recent invocation with requested nonce. Ignore erro
rs here, | 139 // Find most recent invocation with requested nonce. Ignore erro
rs here, |
| 142 // since GetInvocationsByNonce can return only transient ones. | 140 // since GetInvocationsByNonce can return only transient ones. |
| 143 » » invs, _ := e.GetInvocationsByNonce(c, invNonce) | 141 » » invs, _ := e.GetVisibleInvocationsByNonce(c, invNonce) |
| 144 bestTS := time.Time{} | 142 bestTS := time.Time{} |
| 145 for _, inv := range invs { | 143 for _, inv := range invs { |
| 146 if inv.JobKey.StringID() == jobID && inv.Started.Sub(bes
tTS) > 0 { | 144 if inv.JobKey.StringID() == jobID && inv.Started.Sub(bes
tTS) > 0 { |
| 147 invID = inv.ID | 145 invID = inv.ID |
| 148 bestTS = inv.Started | 146 bestTS = inv.Started |
| 149 } | 147 } |
| 150 } | 148 } |
| 151 } | 149 } |
| 152 | 150 |
| 153 if invID != 0 { | 151 if invID != 0 { |
| 154 http.Redirect(w, r, fmt.Sprintf("/jobs/%s/%s/%d", projectID, job
Name, invID), http.StatusFound) | 152 http.Redirect(w, r, fmt.Sprintf("/jobs/%s/%s/%d", projectID, job
Name, invID), http.StatusFound) |
| 155 } else { | 153 } else { |
| 156 genericReply(nil) // deadline | 154 genericReply(nil) // deadline |
| 157 } | 155 } |
| 158 } | 156 } |
| 159 | 157 |
| 160 func pauseJobAction(c *router.Context) { | 158 func pauseJobAction(c *router.Context) { |
| 161 handleJobAction(c, func(jobID string) error { | 159 handleJobAction(c, func(jobID string) error { |
| 162 » » who := auth.CurrentIdentity(c.Context) | 160 » » return config(c.Context).Engine.PauseJob(c.Context, jobID) |
| 163 » » return config(c.Context).Engine.PauseJob(c.Context, jobID, who) | |
| 164 }) | 161 }) |
| 165 } | 162 } |
| 166 | 163 |
| 167 func resumeJobAction(c *router.Context) { | 164 func resumeJobAction(c *router.Context) { |
| 168 handleJobAction(c, func(jobID string) error { | 165 handleJobAction(c, func(jobID string) error { |
| 169 » » who := auth.CurrentIdentity(c.Context) | 166 » » return config(c.Context).Engine.ResumeJob(c.Context, jobID) |
| 170 » » return config(c.Context).Engine.ResumeJob(c.Context, jobID, who) | |
| 171 }) | 167 }) |
| 172 } | 168 } |
| 173 | 169 |
| 174 func abortJobAction(c *router.Context) { | 170 func abortJobAction(c *router.Context) { |
| 175 handleJobAction(c, func(jobID string) error { | 171 handleJobAction(c, func(jobID string) error { |
| 176 » » who := auth.CurrentIdentity(c.Context) | 172 » » return config(c.Context).Engine.AbortJob(c.Context, jobID) |
| 177 » » return config(c.Context).Engine.AbortJob(c.Context, jobID, who) | |
| 178 }) | 173 }) |
| 179 } | 174 } |
| 180 | 175 |
| 181 func handleJobAction(c *router.Context, cb func(string) error) { | 176 func handleJobAction(c *router.Context, cb func(string) error) { |
| 182 projectID := c.Params.ByName("ProjectID") | 177 projectID := c.Params.ByName("ProjectID") |
| 183 jobName := c.Params.ByName("JobName") | 178 jobName := c.Params.ByName("JobName") |
| 184 » if !acl.IsJobOwner(c.Context, projectID, jobName) { | 179 » switch err := cb(projectID + "/" + jobName); { |
| 180 » case err == engine.ErrNoOwnerPermission: |
| 185 http.Error(c.Writer, "Forbidden", 403) | 181 http.Error(c.Writer, "Forbidden", 403) |
| 186 » » return | 182 » case err != nil: |
| 183 » » panic(err) |
| 184 » default: |
| 185 » » http.Redirect(c.Writer, c.Request, fmt.Sprintf("/jobs/%s/%s", pr
ojectID, jobName), http.StatusFound) |
| 187 } | 186 } |
| 188 if err := cb(projectID + "/" + jobName); err != nil { | |
| 189 panic(err) | |
| 190 } | |
| 191 http.Redirect(c.Writer, c.Request, fmt.Sprintf("/jobs/%s/%s", projectID,
jobName), http.StatusFound) | |
| 192 } | 187 } |
| OLD | NEW |