mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-28 23:38:25 +00:00
Detail view (#47)
* Adding the ability to get lock data and show it using the detail view in the ui for boltdb * adding modal style * Adding new modal based discard ui * Adding detail view and get lock funtionality with unlocking with the UI * lots of clean up after review * using jquery most places now * missed in merge * this should cause a build failure * moving e2e test as part of the test override step so they run if unit tests fail * fixing boltdb tests * turns out you can't fail fast in circleci * don't compare locks
This commit is contained in:
2
Makefile
2
Makefile
@@ -1,6 +1,6 @@
|
||||
BUILD_ID := $(shell git rev-parse --short HEAD 2>/dev/null || echo no-commit-id)
|
||||
WORKSPACE := $(shell pwd)
|
||||
PKG := $(shell go list ./... | grep -v e2e | grep -v vendor)
|
||||
PKG := $(shell go list ./... | grep -v e2e | grep -v vendor | grep -v static)
|
||||
|
||||
.PHONY: test
|
||||
|
||||
|
||||
@@ -21,9 +21,9 @@ test:
|
||||
override:
|
||||
# Run tests
|
||||
- cd "${WORKDIR}" && ./scripts/build.sh
|
||||
|
||||
# Run e2e tests
|
||||
|
||||
post:
|
||||
# Run e2e tests
|
||||
- cd "${WORKDIR}" && ./scripts/e2e-deps.sh
|
||||
# Start atlantis server
|
||||
- cd "${WORKDIR}/e2e" && ./atlantis server --gh-user="$GITHUB_USERNAME" --gh-password="$GITHUB_PASSWORD" --data-dir="/tmp" --require-approval=false --plan-backend="file" --log-level="debug" &> /tmp/atlantis-server.log:
|
||||
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/hootsuite/atlantis/models"
|
||||
"github.com/pkg/errors"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/hootsuite/atlantis/models"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Backend struct {
|
||||
@@ -139,7 +140,7 @@ func (b Backend) UnlockByPull(repoFullName string, pullNum int) ([]models.Projec
|
||||
for k, v := c.Seek([]byte(repoFullName)); k != nil && bytes.HasPrefix(k, []byte(repoFullName)); k, v = c.Next() {
|
||||
var lock models.ProjectLock
|
||||
if err := json.Unmarshal(v, &lock); err != nil {
|
||||
return errors.Wrapf(err, "failed to deserialize lock at key %q", string(k))
|
||||
return errors.Wrapf(err, "deserializing lock at key %q", string(k))
|
||||
}
|
||||
if lock.Pull.Num == pullNum {
|
||||
locks = append(locks, lock)
|
||||
@@ -157,6 +158,26 @@ func (b Backend) UnlockByPull(repoFullName string, pullNum int) ([]models.Projec
|
||||
return locks, nil
|
||||
}
|
||||
|
||||
func (b Backend) GetLock(p models.Project, env string) (models.ProjectLock, error) {
|
||||
key := b.key(p, env)
|
||||
var lockBytes []byte
|
||||
err := b.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket(b.bucket)
|
||||
lockBytes = b.Get([]byte(key))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return models.ProjectLock{}, errors.Wrap(err, "getting lock data")
|
||||
}
|
||||
|
||||
var lock models.ProjectLock
|
||||
if err := json.Unmarshal(lockBytes, &lock); err != nil {
|
||||
return models.ProjectLock{}, errors.Wrapf(err, "deserializing lock at key %q", key)
|
||||
}
|
||||
|
||||
return lock, nil
|
||||
}
|
||||
|
||||
func (b Backend) key(p models.Project, env string) string {
|
||||
return fmt.Sprintf("%s/%s/%s", p.RepoFullName, p.Path, env)
|
||||
}
|
||||
|
||||
@@ -3,15 +3,17 @@ package boltdb_test
|
||||
import (
|
||||
. "github.com/hootsuite/atlantis/testing_util"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/pkg/errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/hootsuite/atlantis/locking/boltdb"
|
||||
"github.com/hootsuite/atlantis/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
var lockBucket = "bucket"
|
||||
@@ -25,9 +27,9 @@ var lock = models.ProjectLock{
|
||||
User: models.User{
|
||||
Username: "lkysow",
|
||||
},
|
||||
Env: env,
|
||||
Env: env,
|
||||
Project: project,
|
||||
Time: time.Now(),
|
||||
Time: time.Now(),
|
||||
}
|
||||
|
||||
func TestListNoLocks(t *testing.T) {
|
||||
@@ -158,8 +160,9 @@ func TestUnlockingNoLocks(t *testing.T) {
|
||||
t.Log("unlocking with no locks should succeed")
|
||||
db, b := newTestDB()
|
||||
defer cleanupDB(db)
|
||||
_, err := b.Unlock(project, env)
|
||||
|
||||
Ok(t, b.Unlock(project, env))
|
||||
Ok(t, err)
|
||||
}
|
||||
|
||||
func TestUnlocking(t *testing.T) {
|
||||
@@ -168,7 +171,8 @@ func TestUnlocking(t *testing.T) {
|
||||
defer cleanupDB(db)
|
||||
|
||||
b.TryLock(lock)
|
||||
Ok(t, b.Unlock(project, env))
|
||||
_, err := b.Unlock(project, env)
|
||||
Ok(t, err)
|
||||
|
||||
// should be no locks listed
|
||||
ls, err := b.List()
|
||||
@@ -204,10 +208,14 @@ func TestUnlockingMultiple(t *testing.T) {
|
||||
b.TryLock(new3)
|
||||
|
||||
// now try and unlock them
|
||||
Ok(t, b.Unlock(new3.Project, new3.Env))
|
||||
Ok(t, b.Unlock(new2.Project, env))
|
||||
Ok(t, b.Unlock(new.Project, env))
|
||||
Ok(t, b.Unlock(project, env))
|
||||
_, err := b.Unlock(new3.Project, new3.Env)
|
||||
Ok(t, err)
|
||||
_, err = b.Unlock(new2.Project, env)
|
||||
Ok(t, err)
|
||||
_, err = b.Unlock(new.Project, env)
|
||||
Ok(t, err)
|
||||
_, err = b.Unlock(project, env)
|
||||
Ok(t, err)
|
||||
|
||||
// should be none left
|
||||
ls, err := b.List()
|
||||
@@ -220,7 +228,7 @@ func TestUnlockByPullNone(t *testing.T) {
|
||||
db, b := newTestDB()
|
||||
defer cleanupDB(db)
|
||||
|
||||
err := b.UnlockByPull("any/repo", 1)
|
||||
_, err := b.UnlockByPull("any/repo", 1)
|
||||
Ok(t, err)
|
||||
}
|
||||
|
||||
@@ -233,7 +241,7 @@ func TestUnlockByPullOne(t *testing.T) {
|
||||
|
||||
t.Log("...delete nothing when its the same repo but a different pull")
|
||||
{
|
||||
err := b.UnlockByPull(project.RepoFullName, pullNum+1)
|
||||
_, err := b.UnlockByPull(project.RepoFullName, pullNum+1)
|
||||
Ok(t, err)
|
||||
ls, err := b.List()
|
||||
Ok(t, err)
|
||||
@@ -241,7 +249,7 @@ func TestUnlockByPullOne(t *testing.T) {
|
||||
}
|
||||
t.Log("...delete nothing when its the same pull but a different repo")
|
||||
{
|
||||
err := b.UnlockByPull("different/repo", pullNum)
|
||||
_, err := b.UnlockByPull("different/repo", pullNum)
|
||||
Ok(t, err)
|
||||
ls, err := b.List()
|
||||
Ok(t, err)
|
||||
@@ -249,7 +257,7 @@ func TestUnlockByPullOne(t *testing.T) {
|
||||
}
|
||||
t.Log("...delete the lock when its the same repo and pull")
|
||||
{
|
||||
err := b.UnlockByPull(project.RepoFullName, pullNum)
|
||||
_, err := b.UnlockByPull(project.RepoFullName, pullNum)
|
||||
Ok(t, err)
|
||||
ls, err := b.List()
|
||||
Ok(t, err)
|
||||
@@ -263,9 +271,10 @@ func TestUnlockByPullAfterUnlock(t *testing.T) {
|
||||
defer cleanupDB(db)
|
||||
_, _, err := b.TryLock(lock)
|
||||
Ok(t, err)
|
||||
Ok(t, b.Unlock(project, env))
|
||||
_, err = b.Unlock(project, env)
|
||||
Ok(t, err)
|
||||
|
||||
err = b.UnlockByPull(project.RepoFullName, pullNum)
|
||||
_, err = b.UnlockByPull(project.RepoFullName, pullNum)
|
||||
Ok(t, err)
|
||||
ls, err := b.List()
|
||||
Ok(t, err)
|
||||
@@ -295,13 +304,25 @@ func TestUnlockByPullMatching(t *testing.T) {
|
||||
Equals(t, 3, len(ls))
|
||||
|
||||
// should all be unlocked
|
||||
err = b.UnlockByPull(project.RepoFullName, pullNum)
|
||||
_, err = b.UnlockByPull(project.RepoFullName, pullNum)
|
||||
Ok(t, err)
|
||||
ls, err = b.List()
|
||||
Ok(t, err)
|
||||
Equals(t, 0, len(ls))
|
||||
}
|
||||
|
||||
// todo: Write more tests for getting lock data
|
||||
func TestGetLock(t *testing.T) {
|
||||
t.Log("get data for a existing lock")
|
||||
db, b := newTestDB()
|
||||
defer cleanupDB(db)
|
||||
_, _, err := b.TryLock(lock)
|
||||
Ok(t, err)
|
||||
|
||||
_, err = b.GetLock(project, env)
|
||||
Ok(t, err)
|
||||
}
|
||||
|
||||
// newTestDB returns a TestDB using a temporary path.
|
||||
func newTestDB() (*bolt.DB, *boltdb.Backend) {
|
||||
// Retrieve a temporary path.
|
||||
|
||||
@@ -2,6 +2,9 @@ package dynamodb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/client"
|
||||
"github.com/aws/aws-sdk-go/service/dynamodb"
|
||||
@@ -9,8 +12,6 @@ import (
|
||||
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
|
||||
"github.com/hootsuite/atlantis/models"
|
||||
"github.com/pkg/errors"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Backend struct {
|
||||
@@ -23,18 +24,18 @@ type Backend struct {
|
||||
// and also so any changes to models won't affect
|
||||
// how we're storing our data (or will at least cause a compile error)
|
||||
type dynamoLock struct {
|
||||
LockKey string
|
||||
RepoFullName string
|
||||
Path string
|
||||
LockKey string
|
||||
RepoFullName string
|
||||
Path string
|
||||
PullNum int
|
||||
PullHeadCommit string
|
||||
PullBaseCommit string
|
||||
PullURL string
|
||||
PullBranch string
|
||||
PullAuthor string
|
||||
UserUsername string
|
||||
Env string
|
||||
Time time.Time
|
||||
UserUsername string
|
||||
Env string
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
func New(lockTable string, p client.ConfigProvider) Backend {
|
||||
@@ -142,6 +143,30 @@ func (b Backend) List() ([]models.ProjectLock, error) {
|
||||
return locks, errors.Wrap(err, "scanning dynamodb")
|
||||
}
|
||||
|
||||
func (b Backend) GetLock(p models.Project, env string) (models.ProjectLock, error) {
|
||||
key := b.key(p, env)
|
||||
params := &dynamodb.GetItemInput{
|
||||
Key: map[string]*dynamodb.AttributeValue{
|
||||
"LockKey": {
|
||||
S: aws.String(key),
|
||||
},
|
||||
},
|
||||
TableName: aws.String(b.LockTable),
|
||||
ConsistentRead: aws.Bool(true),
|
||||
}
|
||||
item, err := b.DB.GetItem(params)
|
||||
if err != nil {
|
||||
return models.ProjectLock{}, errors.Wrapf(err, "getting item %q", item)
|
||||
}
|
||||
|
||||
var dynamoDBLock dynamoLock
|
||||
if err := dynamodbattribute.UnmarshalMap(item.Item, &dynamoDBLock); err != nil {
|
||||
return models.ProjectLock{}, errors.Wrap(err, "found a lock at that key but it could not be deserialized. We suggest manually deleting this key from DynamoDB")
|
||||
}
|
||||
|
||||
return b.fromDynamo(dynamoDBLock), nil
|
||||
}
|
||||
|
||||
func (b Backend) UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error) {
|
||||
params := &dynamodb.ScanInput{
|
||||
ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
|
||||
@@ -186,39 +211,39 @@ func (b Backend) UnlockByPull(repoFullName string, pullNum int) ([]models.Projec
|
||||
|
||||
func (b Backend) toDynamo(key string, l models.ProjectLock) dynamoLock {
|
||||
return dynamoLock{
|
||||
LockKey: key,
|
||||
RepoFullName: l.Project.RepoFullName,
|
||||
Path: l.Project.Path,
|
||||
PullNum: l.Pull.Num,
|
||||
LockKey: key,
|
||||
RepoFullName: l.Project.RepoFullName,
|
||||
Path: l.Project.Path,
|
||||
PullNum: l.Pull.Num,
|
||||
PullHeadCommit: l.Pull.HeadCommit,
|
||||
PullBaseCommit: l.Pull.BaseCommit,
|
||||
PullURL: l.Pull.URL,
|
||||
PullBranch: l.Pull.Branch,
|
||||
PullAuthor: l.Pull.Author,
|
||||
UserUsername: l.User.Username,
|
||||
Env: l.Env,
|
||||
Time: time.Now(),
|
||||
PullURL: l.Pull.URL,
|
||||
PullBranch: l.Pull.Branch,
|
||||
PullAuthor: l.Pull.Author,
|
||||
UserUsername: l.User.Username,
|
||||
Env: l.Env,
|
||||
Time: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (b Backend) fromDynamo(d dynamoLock) models.ProjectLock {
|
||||
return models.ProjectLock{
|
||||
Pull: models.PullRequest{
|
||||
Author: d.PullAuthor,
|
||||
Branch: d.PullBranch,
|
||||
URL: d.PullURL,
|
||||
Author: d.PullAuthor,
|
||||
Branch: d.PullBranch,
|
||||
URL: d.PullURL,
|
||||
BaseCommit: d.PullBaseCommit,
|
||||
HeadCommit: d.PullHeadCommit,
|
||||
Num: d.PullNum,
|
||||
Num: d.PullNum,
|
||||
},
|
||||
User: models.User{
|
||||
Username: d.UserUsername,
|
||||
},
|
||||
Project: models.Project{
|
||||
RepoFullName: d.RepoFullName,
|
||||
Path: d.Path,
|
||||
Path: d.Path,
|
||||
},
|
||||
Time: d.Time,
|
||||
Env: d.Env,
|
||||
Env: d.Env,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,22 +3,24 @@ package locking
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/hootsuite/atlantis/models"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/hootsuite/atlantis/models"
|
||||
)
|
||||
|
||||
type Backend interface {
|
||||
TryLock(lock models.ProjectLock) (bool, models.ProjectLock, error)
|
||||
Unlock(project models.Project, env string) (*models.ProjectLock, error)
|
||||
List() ([]models.ProjectLock, error)
|
||||
GetLock(project models.Project, env string) (models.ProjectLock, error)
|
||||
UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error)
|
||||
}
|
||||
|
||||
type TryLockResponse struct {
|
||||
LockAcquired bool
|
||||
CurrLock models.ProjectLock
|
||||
LockKey string
|
||||
LockAcquired bool
|
||||
CurrLock models.ProjectLock
|
||||
LockKey string
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
@@ -36,11 +38,11 @@ var keyRegex = regexp.MustCompile(`^(.*?\/.*?)\/(.*)\/(.*)$`)
|
||||
|
||||
func (c *Client) TryLock(p models.Project, env string, pull models.PullRequest, user models.User) (TryLockResponse, error) {
|
||||
lock := models.ProjectLock{
|
||||
Env: env,
|
||||
Time: time.Now(),
|
||||
Env: env,
|
||||
Time: time.Now(),
|
||||
Project: p,
|
||||
User: user,
|
||||
Pull: pull,
|
||||
User: user,
|
||||
Pull: pull,
|
||||
}
|
||||
lockAcquired, currLock, err := c.backend.TryLock(lock)
|
||||
if err != nil {
|
||||
@@ -50,11 +52,12 @@ func (c *Client) TryLock(p models.Project, env string, pull models.PullRequest,
|
||||
}
|
||||
|
||||
func (c *Client) Unlock(key string) (*models.ProjectLock, error) {
|
||||
matches := keyRegex.FindStringSubmatch(key)
|
||||
if len(matches) != 4 {
|
||||
return nil, errors.New("invalid key format")
|
||||
project, env, err := c.lockKeyToProjectEnv(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.backend.Unlock(models.Project{matches[1], matches[2]}, matches[3])
|
||||
|
||||
return c.backend.Unlock(project, env)
|
||||
}
|
||||
|
||||
func (c *Client) List() (map[string]models.ProjectLock, error) {
|
||||
@@ -73,6 +76,29 @@ func (c *Client) UnlockByPull(repoFullName string, pullNum int) ([]models.Projec
|
||||
return c.backend.UnlockByPull(repoFullName, pullNum)
|
||||
}
|
||||
|
||||
func (c *Client) GetLock(key string) (models.ProjectLock, error) {
|
||||
project, env, err := c.lockKeyToProjectEnv(key)
|
||||
if err != nil {
|
||||
return models.ProjectLock{}, err
|
||||
}
|
||||
|
||||
projectLock, err := c.backend.GetLock(project, env)
|
||||
if err != nil {
|
||||
return models.ProjectLock{}, err
|
||||
}
|
||||
|
||||
return projectLock, nil
|
||||
}
|
||||
|
||||
func (c *Client) key(p models.Project, env string) string {
|
||||
return fmt.Sprintf("%s/%s/%s", p.RepoFullName, p.Path, env)
|
||||
}
|
||||
|
||||
func (c *Client) lockKeyToProjectEnv(key string) (models.Project, string, error) {
|
||||
matches := keyRegex.FindStringSubmatch(key)
|
||||
if len(matches) != 4 {
|
||||
return models.Project{}, "", errors.New("invalid key format")
|
||||
}
|
||||
|
||||
return models.Project{matches[1], matches[2]}, matches[3], nil
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
run_unit_test() {
|
||||
echo "Running unit tests: 'make test'"
|
||||
make test
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hootsuite/atlantis/logging"
|
||||
"github.com/hootsuite/atlantis/recovery"
|
||||
)
|
||||
@@ -73,8 +74,8 @@ func (s *CommandHandler) ExecuteCommand(ctx *CommandContext) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CommandHandler) SetDeleteLockURL(f func(id string) (url string)) {
|
||||
s.planExecutor.DeleteLockURL = f
|
||||
func (s *CommandHandler) SetLockURL(f func(id string) (url string)) {
|
||||
s.planExecutor.LockURL = f
|
||||
}
|
||||
|
||||
// recover logs and creates a comment on the pull request for panics
|
||||
|
||||
@@ -2,17 +2,18 @@ package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/hootsuite/atlantis/locking"
|
||||
"github.com/hootsuite/atlantis/logging"
|
||||
"github.com/hootsuite/atlantis/models"
|
||||
"github.com/hootsuite/atlantis/plan"
|
||||
"github.com/pkg/errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/hootsuite/atlantis/locking"
|
||||
"github.com/hootsuite/atlantis/logging"
|
||||
"github.com/hootsuite/atlantis/models"
|
||||
"github.com/hootsuite/atlantis/plan"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// PlanExecutor handles everything related to running the Terraform plan including integration with S3, Terraform, and GitHub
|
||||
@@ -26,9 +27,9 @@ type PlanExecutor struct {
|
||||
terraform *TerraformClient
|
||||
githubCommentRenderer *GithubCommentRenderer
|
||||
lockingClient *locking.Client
|
||||
// DeleteLockURL is a function that given a lock id will return a url for deleting the lock
|
||||
DeleteLockURL func(id string) (url string)
|
||||
planBackend plan.Backend
|
||||
// LockURL is a function that given a lock id will return a url for detail view
|
||||
LockURL func(id string) (url string)
|
||||
planBackend plan.Backend
|
||||
}
|
||||
|
||||
/** Result Types **/
|
||||
@@ -314,7 +315,7 @@ func (p *PlanExecutor) plan(
|
||||
Status: Success,
|
||||
Result: PlanSuccess{
|
||||
TerraformOutput: output,
|
||||
LockURL: p.DeleteLockURL(lockAttempt.LockKey),
|
||||
LockURL: p.LockURL(lockAttempt.LockKey),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
|
||||
"time"
|
||||
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/elazarl/go-bindata-assetfs"
|
||||
"github.com/google/go-github/github"
|
||||
@@ -27,7 +29,6 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/negroni"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -220,17 +221,12 @@ func (s *Server) Start() error {
|
||||
s.router.PathPrefix("/static/").Handler(http.FileServer(&assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo}))
|
||||
s.router.HandleFunc("/hooks", s.postHooks).Methods("POST")
|
||||
s.router.HandleFunc("/locks", s.deleteLock).Methods("DELETE").Queries("id", "{id:.*}")
|
||||
// todo: remove this route when there is a detail view
|
||||
// right now we need this route because from the pull request comment in GitHub only a GET request can be made
|
||||
// in the future, the pull discard link will link to the detail view which will have a Delete button which will
|
||||
// make an real DELETE call but we don't have a detail view right now
|
||||
deleteLockRoute := s.router.HandleFunc("/locks", s.deleteLock).Queries("id", "{id}", "method", "DELETE").Methods("GET").Name(deleteLockRoute)
|
||||
|
||||
// function that planExecutor can use to construct delete lock urls
|
||||
lockRoute := s.router.HandleFunc("/lock", s.lock).Methods("GET").Queries("id", "{id}")
|
||||
// function that planExecutor can use to construct detail view url
|
||||
// injecting this here because this is the earliest routes are created
|
||||
s.commandHandler.SetDeleteLockURL(func(lockID string) string {
|
||||
s.commandHandler.SetLockURL(func(lockID string) string {
|
||||
// ignoring error since guaranteed to succeed if "id" is specified
|
||||
u, _ := deleteLockRoute.URL("id", url.QueryEscape(lockID))
|
||||
u, _ := lockRoute.URL("id", url.QueryEscape(lockID))
|
||||
return s.atlantisURL + u.RequestURI()
|
||||
})
|
||||
n := negroni.New(&negroni.Recovery{
|
||||
@@ -253,16 +249,16 @@ func (s *Server) index(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
type lock struct {
|
||||
UnlockURL string
|
||||
LockURL string
|
||||
RepoFullName string
|
||||
PullNum int
|
||||
Time time.Time
|
||||
}
|
||||
var results []lock
|
||||
for id, v := range locks {
|
||||
u, _ := s.router.Get(deleteLockRoute).URL("id", url.QueryEscape(id))
|
||||
results = append(results, lock{
|
||||
UnlockURL: u.String(),
|
||||
// todo: make LockURL use the router to get /lock endpoint
|
||||
LockURL: fmt.Sprintf("/lock?id=%s", url.QueryEscape(id)),
|
||||
RepoFullName: v.Project.RepoFullName,
|
||||
PullNum: v.Pull.Num,
|
||||
Time: v.Time,
|
||||
@@ -271,6 +267,54 @@ func (s *Server) index(w http.ResponseWriter, r *http.Request) {
|
||||
indexTemplate.Execute(w, results)
|
||||
}
|
||||
|
||||
func (s *Server) lock(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := mux.Vars(r)["id"]
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprint(w, "no lock id in request")
|
||||
}
|
||||
// get details for lock id
|
||||
idUnencoded, err := url.QueryUnescape(id)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprint(w, "invalid lock id")
|
||||
}
|
||||
|
||||
// for the given lock key get lock data
|
||||
lock, err := s.lockingClient.GetLock(idUnencoded)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprint(w, err.Error())
|
||||
}
|
||||
|
||||
type lockData struct {
|
||||
UnlockURL string
|
||||
LockKeyEncoded string
|
||||
LockKey string
|
||||
RepoOwner string
|
||||
RepoName string
|
||||
PullRequestLink string
|
||||
LockedBy string
|
||||
Environment string
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// extract the repo owner and repo name
|
||||
repo := strings.Split(lock.Project.RepoFullName, "/")
|
||||
|
||||
l := lockData{
|
||||
LockKeyEncoded: id,
|
||||
LockKey: idUnencoded,
|
||||
RepoOwner: repo[0],
|
||||
RepoName: repo[1],
|
||||
PullRequestLink: lock.Pull.URL,
|
||||
LockedBy: lock.Pull.Author,
|
||||
Environment: lock.Env,
|
||||
}
|
||||
|
||||
lockTemplate.Execute(w, l)
|
||||
}
|
||||
|
||||
func (s *Server) deleteLock(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := mux.Vars(r)["id"]
|
||||
if !ok {
|
||||
|
||||
@@ -13,6 +13,15 @@ var indexTemplate = template.Must(template.New("index.html.tmpl").Parse(`
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="/static/js/jquery-3.2.1.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$("p.js-discard-success").toggle(document.URL.indexOf("discard=true") !== -1);
|
||||
});
|
||||
setTimeout(function() {
|
||||
$("p.js-discard-success").fadeOut('slow');
|
||||
}, 5000); // <-- time in milliseconds
|
||||
</script>
|
||||
<link rel="stylesheet" href="/static/css/normalize.css">
|
||||
<link rel="stylesheet" href="/static/css/skeleton.css">
|
||||
<link rel="stylesheet" href="/static/css/custom.css">
|
||||
@@ -22,7 +31,8 @@ var indexTemplate = template.Must(template.New("index.html.tmpl").Parse(`
|
||||
<div class="container">
|
||||
<section class="header">
|
||||
<a title="atlantis" href="/"><img src="/static/images/atlantis-icon.png"/></a>
|
||||
<p style="font-family: monospace, monospace; font-size: 1.1em; text-align: center;">atlantis</p>
|
||||
<p class="title-heading">atlantis</p>
|
||||
<p class="js-discard-success"><strong>Plan discarded and unlocked!</strong></p>
|
||||
</section>
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
@@ -31,15 +41,16 @@ var indexTemplate = template.Must(template.New("index.html.tmpl").Parse(`
|
||||
<div class="navbar-spacer"></div>
|
||||
<br>
|
||||
<section>
|
||||
<p style="font-family: monospace, monospace; font-size: 1.0em; text-align: center;"><strong>Environments</strong></p>
|
||||
<p class="title-heading small"><strong>Environments</strong></p>
|
||||
{{ if . }}
|
||||
{{ range . }}
|
||||
<div class="twelve columns button content lock-row">
|
||||
<a href="{{.LockURL}}">
|
||||
<div class="twelve columns button content lock-row">
|
||||
<div class="list-title">{{.RepoFullName}} - <span class="heading-font-size">#{{.PullNum}}</span></div>
|
||||
<div class="list-unlock"><button class="unlock"><a class="unlock-link" href="{{.UnlockURL}}">Unlock</a></button></div>
|
||||
<div class="list-status"><code>Locked</code></div>
|
||||
<div class="list-timestamp"><span class="heading-font-size">{{.Time}}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<p class="placeholder">No environments found.</p>
|
||||
@@ -49,3 +60,102 @@ var indexTemplate = template.Must(template.New("index.html.tmpl").Parse(`
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
|
||||
var lockTemplate = template.Must(template.New("lock.html.tmpl").Parse(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>atlantis</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/static/css/normalize.css">
|
||||
<link rel="stylesheet" href="/static/css/skeleton.css">
|
||||
<link rel="stylesheet" href="/static/css/custom.css">
|
||||
<link rel="icon" type="image/png" href="/static/images/atlantis-icon.png">
|
||||
<script src="/static/js/jquery-3.2.1.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<section class="header">
|
||||
<a title="atlantis" href="/"><img src="/static/images/atlantis-icon.png"/></a>
|
||||
<p class="title-heading">atlantis</p>
|
||||
<p class="title-heading"><strong>{{.LockKey}}</strong> <code>Locked</code></p>
|
||||
</section>
|
||||
<div class="navbar-spacer"></div>
|
||||
<br>
|
||||
<section>
|
||||
<div class="eight columns">
|
||||
<h6><code>Repo Owner</code>: <strong>{{.RepoOwner}}</strong></h6>
|
||||
<h6><code>Repo Name</code>: <strong>{{.RepoName}}</strong></h6>
|
||||
<h6><code>Pull Request Link</code>: <a href="{{.PullRequestLink}}" target="_blank"><strong>{{.PullRequestLink}}</strong></a></h6>
|
||||
<h6><code>Locked By</code>: <strong>{{.LockedBy}}</strong></h6>
|
||||
<h6><code>Environment</code>: <strong>{{.Environment}}</strong></h6>
|
||||
<br>
|
||||
</div>
|
||||
<div class="four columns">
|
||||
<a class="button button-default" id="discardPlanUnlock">Discard Plan & Unlock</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div id="discardMessageModal" class="modal">
|
||||
<!-- Modal content -->
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<span class="close">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p><strong>Are you sure you want to discard the plan and unlock?</strong></p>
|
||||
<input class="button-primary" id="discardYes" type="submit" value="Yes" data="{{.LockKeyEncoded}}">
|
||||
<input type="button" class="cancel" value="Cancel">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Get the modal
|
||||
var modal = $("#discardMessageModal");
|
||||
|
||||
// Get the button that opens the modal
|
||||
var btn = $("#discardPlanUnlock");
|
||||
var btnDiscard = $("#discardYes");
|
||||
var lockId = btnDiscard.attr('data');
|
||||
|
||||
// Get the <span> element that closes the modal
|
||||
// using document.getElementsByClassName since jquery $("close") doesn't seem to work for btn click events
|
||||
var span = document.getElementsByClassName("close")[0];
|
||||
var cancelBtn = document.getElementsByClassName("cancel")[0];
|
||||
|
||||
// When the user clicks the button, open the modal
|
||||
btn.click(function() {
|
||||
modal.css("display", "block");
|
||||
});
|
||||
|
||||
// When the user clicks on <span> (x), close the modal
|
||||
span.onclick = function() {
|
||||
modal.css("display", "none");
|
||||
}
|
||||
cancelBtn.onclick = function() {
|
||||
modal.css("display", "none");
|
||||
}
|
||||
|
||||
btnDiscard.click(function() {
|
||||
$.ajax({
|
||||
url: '/locks?id='+lockId,
|
||||
type: 'DELETE',
|
||||
success: function(result) {
|
||||
window.location.replace("/?discard=true");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// When the user clicks anywhere outside of the modal, close it
|
||||
window.onclick = function(event) {
|
||||
if (event.target == modal) {
|
||||
modal.css("display", "none");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
|
||||
@@ -246,7 +246,66 @@ tbody {
|
||||
.lock-row{
|
||||
cursor: default;
|
||||
}
|
||||
.unlock-link{
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
|
||||
/* The Modal (background) */
|
||||
.modal {
|
||||
display: none; /* Hidden by default */
|
||||
position: fixed; /* Stay in place */
|
||||
z-index: 1; /* Sit on top */
|
||||
padding-top: 100px; /* Location of the box */
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%; /* Full width */
|
||||
height: 100%; /* Full height */
|
||||
overflow: auto; /* Enable scroll if needed */
|
||||
background-color: rgb(0,0,0); /* Fallback color */
|
||||
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
|
||||
}
|
||||
|
||||
/* Modal Header */
|
||||
.modal-header {
|
||||
padding: 8px 12px;
|
||||
background-color: #222222;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Modal Body */
|
||||
.modal-body {padding: 18px 16px;}
|
||||
|
||||
/* Modal Content */
|
||||
.modal-content {
|
||||
position: relative;
|
||||
background-color: #fefefe;
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
border: 1px solid #888;
|
||||
width: 50%;
|
||||
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);
|
||||
-webkit-animation-name: animatetop;
|
||||
-webkit-animation-duration: 0.4s;
|
||||
animation-name: animatetop;
|
||||
animation-duration: 0.4s
|
||||
}
|
||||
|
||||
/* Add Animation */
|
||||
@-webkit-keyframes animatetop {
|
||||
from {top: -300px; opacity: 0}
|
||||
to {top: 0; opacity: 1}
|
||||
}
|
||||
|
||||
@keyframes animatetop {
|
||||
from {top: -300px; opacity: 0}
|
||||
to {top: 0; opacity: 1}
|
||||
}
|
||||
|
||||
.js-discard-success {
|
||||
font-family: monospace, monospace; font-size: 1.1em; text-align: center;
|
||||
}
|
||||
|
||||
.title-heading {
|
||||
font-family: monospace, monospace; font-size: 1.1em; text-align: center;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 1.0em;
|
||||
}
|
||||
|
||||
4
static/js/jquery-3.2.1.min.js
vendored
Normal file
4
static/js/jquery-3.2.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user