Skip to content

Commit

Permalink
feat(brig): add brig project delete (#512)
Browse files Browse the repository at this point in the history
This adds a command to delete a project.
  • Loading branch information
technosophos authored Jun 23, 2018
1 parent 20c4a0a commit e2419a9
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 12 deletions.
8 changes: 7 additions & 1 deletion Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@
[[constraint]]
branch = "master"
name = "github.com/Masterminds/kitt"

[[constraint]]
name = "github.com/Masterminds/goutils"
version = "1.0.1"
83 changes: 83 additions & 0 deletions brig/cmd/brig/commands/project_delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package commands

import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"

"github.com/spf13/cobra"

"github.com/Azure/brigade/pkg/storage/kube"
)

var projectDeleteUsage = `Delete a project.
This removes the project from brigade by deleting the project secret in Kubernetes.
It does not delete old builds or jobs.
With the -o/--out flag, you can save a local backup copy of this project before deleting
it.
`

var (
projectDeleteOut = ""
projectDeleteDryRun = false
)

func init() {
project.AddCommand(projectDelete)
flags := projectDelete.Flags()
flags.StringVarP(&projectDeleteOut, "out", "o", "", "File where configuration should be saved before deletion")
flags.BoolVarP(&projectDeleteDryRun, "dry-run", "D", false, "Check that the project exists, but don't delete it.")
}

var projectDelete = &cobra.Command{
Use: "delete PROJECT",
Short: "delete a Brigade project",
Long: projectDeleteUsage,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("project name is a required argument")
}
return deleteProject(cmd.OutOrStderr(), args[0])
},
}

func deleteProject(out io.Writer, pid string) error {
c, err := kubeClient()
if err != nil {
return err
}

store := kube.New(c, globalNamespace)
p, err := store.GetProject(pid)
if err != nil {
// To remain idempotent, we just return nil here.
if globalVerbose {
fmt.Fprintf(out, "project not found: %q\n", pid)
}
return nil
}

if projectDeleteOut != "" {
secret, err := kube.SecretFromProject(p)
if err != nil {
return fmt.Errorf("deletion aborted due to conversion error: %s", err)
}
data, err := json.Marshal(secret)
if err != nil {
return fmt.Errorf("deletion aborted due to encoding error: %s", err)
}
if err := ioutil.WriteFile(projectDeleteOut, data, 0755); err != nil {
return fmt.Errorf("deletion aborted because backup could not be created: %s", err)
}
}

if globalVerbose {
fmt.Fprintf(out, "Deleting %s\n", p.ID)
}
return store.DeleteProject(p.ID)
}
31 changes: 22 additions & 9 deletions pkg/storage/kube/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,10 @@ func (s *store) GetProject(id string) (*brigade.Project, error) {
return s.loadProjectConfig(brigade.ProjectID(id))
}

// CreateProject stores a given project.
//
// Project Name is a required field. If not present, Project ID will be calculated
// from project name. This is preferred.
//
// Note that project secrets are not redacted.
func (s *store) CreateProject(project *brigade.Project) error {
// SecretFromProject takes a project and converts it to a Kubernetes Secret.
func SecretFromProject(project *brigade.Project) (v1.Secret, error) {
if project.Name == "" {
return errors.New("project name is required")
return v1.Secret{}, errors.New("project name is required")
}

if project.ID == "" {
Expand All @@ -57,7 +52,7 @@ func (s *store) CreateProject(project *brigade.Project) error {
var secrets map[string]string = project.Secrets
secretsJSON, err := json.Marshal(secrets)
if err != nil {
return err
return v1.Secret{}, err
}

bfmt := func(b bool) string { return fmt.Sprintf("%t", b) }
Expand Down Expand Up @@ -109,11 +104,29 @@ func (s *store) CreateProject(project *brigade.Project) error {
"kubernetes.buildStorageClass": project.Kubernetes.BuildStorageClass,
},
}
return secret, nil
}

// CreateProject stores a given project.
//
// Project Name is a required field. If not present, Project ID will be calculated
// from project name. This is preferred.
//
// Note that project secrets are not redacted.
func (s *store) CreateProject(project *brigade.Project) error {
secret, err := SecretFromProject(project)
if err != nil {
return err
}
_, err = s.client.CoreV1().Secrets(s.namespace).Create(&secret)
return err
}

// DeleteProject deletes a project from storage.
func (s *store) DeleteProject(id string) error {
return s.client.CoreV1().Secrets(s.namespace).Delete(id, &meta.DeleteOptions{})
}

// loadProjectConfig loads a project config from inside of Kubernetes.
//
// The namespace is the namespace where the secret is stored.
Expand Down
14 changes: 14 additions & 0 deletions pkg/storage/kube/project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,20 @@ func TestCreateProject(t *testing.T) {
}
}

func TestDeleteProject(t *testing.T) {
k, s := fakeStore()
p := &brigade.Project{ID: "fake", Name: "fake"}
if err := s.CreateProject(p); err != nil {
t.Fatal(err)
}
if _, err := k.CoreV1().Secrets("default").Get("fake", meta.GetOptions{}); err != nil {
t.Fatal(err)
}
if err := s.DeleteProject("fake"); err != nil {
t.Fatal(err)
}
}

func TestConfigureProject(t *testing.T) {
secret := &v1.Secret{
ObjectMeta: meta.ObjectMeta{
Expand Down
12 changes: 12 additions & 0 deletions pkg/storage/mock/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ func (s *Store) CreateProject(p *brigade.Project) error {
return nil
}

// DeleteProject deletes a project from the internal mock
func (s *Store) DeleteProject(id string) error {
tmp := []*brigade.Project{}
for _, p := range s.ProjectList {
if p.ID == id {
tmp = append(tmp, p)
}
}
s.ProjectList = tmp
return nil
}

// GetProject returns the Project
func (s *Store) GetProject(id string) (*brigade.Project, error) {
for _, proj := range s.ProjectList {
Expand Down
22 changes: 20 additions & 2 deletions pkg/storage/mock/storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ func TestStore(t *testing.T) {
p, _ := m.GetProjects()
assertSame("GetProjects", []*brigade.Project{StubProject}, p)

p2, _ := m.GetProject(StubProject.ID)
assertSame("GetProject", StubProject, p2)
extraProj, _ := m.GetProject(StubProject.ID)
assertSame("GetProject", StubProject, extraProj)

b1, _ := m.GetProjectBuilds(StubProject)
assertSame("GetProjectBuilds", StubBuild, b1[0])
Expand Down Expand Up @@ -80,4 +80,22 @@ func TestStore(t *testing.T) {
if !m.BlockUntilAPICacheSynced(nil) {
t.Fatal("expected to return true")
}

extraProj = &brigade.Project{
Name: "extra",
ID: "extra",
Secrets: map[string]string{},
}
if err := m.CreateProject(extraProj); err != nil {
t.Fatal(err)
}
if len(m.ProjectList) != 2 {
t.Fatal("project was not saved by CreateProject")
}
if err := m.DeleteProject("extra"); err != nil {
t.Fatal(err)
}
if len(m.ProjectList) != 1 {
t.Fatal("project was not deleted by DeleteProject")
}
}
2 changes: 2 additions & 0 deletions pkg/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type ProjectStore interface {
GetProjectBuilds(proj *brigade.Project) ([]*brigade.Build, error)
// CreateProject creates a new project record in storage.
CreateProject(proj *brigade.Project) error
// DeleteProject deletes a project from storage.
DeleteProject(id string) error
}

// Store represents a storage engine for a brigade projects, builds, and jobs.
Expand Down

0 comments on commit e2419a9

Please sign in to comment.