Przeglądaj źródła

project deletion

Alexander Belanger 5 lat temu
rodzic
commit
e9fa098606

+ 29 - 0
cli/cmd/api/project.go

@@ -240,3 +240,32 @@ func (c *Client) CreateProjectServiceAccount(
 
 
 	return bodyResp, nil
 	return bodyResp, nil
 }
 }
+
+// DeleteProjectResponse is the object returned after project deletion
+type DeleteProjectResponse models.ProjectExternal
+
+// DeleteProject deletes a project by id
+func (c *Client) DeleteProject(ctx context.Context, projectID uint) (*DeleteProjectResponse, error) {
+	req, err := http.NewRequest(
+		"DELETE",
+		fmt.Sprintf("%s/projects/%d", c.BaseURL, projectID),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(ctx)
+	bodyResp := &DeleteProjectResponse{}
+
+	if httpErr, err := c.sendRequest(req, bodyResp, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return nil, fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return nil, err
+	}
+
+	return bodyResp, nil
+}

+ 41 - 0
cli/cmd/api/project_test.go

@@ -350,6 +350,47 @@ func TestListProjectClusters(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestDeleteProject(t *testing.T) {
+	email := "delete_project_test@example.com"
+	client := api.NewClient(baseURL, "cookie_delete_project_test.json")
+	user := initUser(email, client, t)
+	client.Login(context.Background(), &api.LoginRequest{
+		Email:    user.Email,
+		Password: "hello1234",
+	})
+	project := initProject("project-test", client, t)
+
+	resp, err := client.DeleteProject(context.Background(), project.ID)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure user is admin and project name is correct
+	if resp.Name != "project-test" {
+		t.Errorf("project name incorrect: expected %s, got %s\n", "project-test", resp.Name)
+	}
+
+	if len(resp.Roles) != 1 {
+		t.Fatalf("project role length is not 1")
+	}
+
+	if resp.Roles[0].Kind != models.RoleAdmin {
+		t.Errorf("project role kind is incorrect: expected %s, got %s\n", models.RoleAdmin, resp.Roles[0].Kind)
+	}
+
+	if resp.Roles[0].UserID != user.ID {
+		t.Errorf("project role user_id is incorrect: expected %d, got %d\n", user.ID, resp.Roles[0].UserID)
+	}
+
+	// make sure that project can no longer be found
+	_, err = client.GetProject(context.Background(), project.ID)
+
+	if err == nil {
+		t.Fatalf("no error returned\n")
+	}
+}
+
 const OIDCAuthWithoutData string = `
 const OIDCAuthWithoutData string = `
 apiVersion: v1
 apiVersion: v1
 clusters:
 clusters:

+ 52 - 2
cli/cmd/project.go

@@ -4,10 +4,13 @@ import (
 	"context"
 	"context"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
+	"strconv"
+	"strings"
 	"text/tabwriter"
 	"text/tabwriter"
 
 
 	"github.com/fatih/color"
 	"github.com/fatih/color"
 	"github.com/porter-dev/porter/cli/cmd/api"
 	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/utils"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 )
 )
 
 
@@ -31,6 +34,19 @@ var createProjectCmd = &cobra.Command{
 	},
 	},
 }
 }
 
 
+var deleteProjectCmd = &cobra.Command{
+	Use:   "delete [id]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Deletes the project with the given id",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, deleteProject)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
 var listProjectCmd = &cobra.Command{
 var listProjectCmd = &cobra.Command{
 	Use:   "list",
 	Use:   "list",
 	Short: "Lists the projects for the logged in user",
 	Short: "Lists the projects for the logged in user",
@@ -58,8 +74,6 @@ var listProjectClustersCmd = &cobra.Command{
 func init() {
 func init() {
 	rootCmd.AddCommand(projectCmd)
 	rootCmd.AddCommand(projectCmd)
 
 
-	projectCmd.AddCommand(createProjectCmd)
-
 	projectCmd.PersistentFlags().StringVar(
 	projectCmd.PersistentFlags().StringVar(
 		&host,
 		&host,
 		"host",
 		"host",
@@ -67,6 +81,10 @@ func init() {
 		"host url of Porter instance",
 		"host url of Porter instance",
 	)
 	)
 
 
+	projectCmd.AddCommand(createProjectCmd)
+
+	projectCmd.AddCommand(deleteProjectCmd)
+
 	projectCmd.AddCommand(listProjectCmd)
 	projectCmd.AddCommand(listProjectCmd)
 
 
 	projectCmd.AddCommand(listProjectClustersCmd)
 	projectCmd.AddCommand(listProjectClustersCmd)
@@ -113,6 +131,38 @@ func listProjects(user *api.AuthCheckResponse, client *api.Client, args []string
 	return nil
 	return nil
 }
 }
 
 
+func deleteProject(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
+	userResp, err := utils.PromptPlaintext(
+		fmt.Sprintf(
+			`Are you sure you'd like to delete the project with id %s? %s `,
+			args[0],
+			color.New(color.FgCyan).Sprintf("[y/n]"),
+		),
+	)
+
+	if err != nil {
+		return err
+	}
+
+	if userResp := strings.ToLower(userResp); userResp == "y" || userResp == "yes" {
+		id, err := strconv.ParseUint(args[0], 10, 64)
+
+		if err != nil {
+			return err
+		}
+
+		resp, err := client.DeleteProject(context.Background(), uint(id))
+
+		if err != nil {
+			return err
+		}
+
+		color.New(color.FgGreen).Printf("Deleted project with name %s and id %d\n", resp.Name, resp.ID)
+	}
+
+	return nil
+}
+
 func listProjectClusters(user *api.AuthCheckResponse, client *api.Client, args []string) error {
 func listProjectClusters(user *api.AuthCheckResponse, client *api.Client, args []string) error {
 	clusters, err := client.ListProjectClusters(context.Background(), getProjectID())
 	clusters, err := client.ListProjectClusters(context.Background(), getProjectID())
 
 

+ 3 - 0
cli/cmd/utils/random_string.go

@@ -11,6 +11,7 @@ const charset = "abcdefghijklmnopqrstuvwxyz" +
 var seededRand *rand.Rand = rand.New(
 var seededRand *rand.Rand = rand.New(
 	rand.NewSource(time.Now().UnixNano()))
 	rand.NewSource(time.Now().UnixNano()))
 
 
+// StringWithCharset returns a random string by pulling from a given charset
 func StringWithCharset(length int, charset string) string {
 func StringWithCharset(length int, charset string) string {
 	b := make([]byte, length)
 	b := make([]byte, length)
 	for i := range b {
 	for i := range b {
@@ -19,6 +20,8 @@ func StringWithCharset(length int, charset string) string {
 	return string(b)
 	return string(b)
 }
 }
 
 
+// String returns a random string, pulling from a standard alphanumeric charset
+// [a-zA-Z0-9]
 func String(length int) string {
 func String(length int) string {
 	return StringWithCharset(length, charset)
 	return StringWithCharset(length, charset)
 }
 }

+ 0 - 62
cmd/app/main.go

@@ -78,65 +78,3 @@ func main() {
 		log.Fatal("Server startup failed", err)
 		log.Fatal("Server startup failed", err)
 	}
 	}
 }
 }
-
-// func upsertAdmin(repo repository.UserRepository, email, pw string) error {
-// 	admUser, err := repo.ReadUserByEmail(email)
-
-// 	// create the user in this case
-// 	if err != nil {
-// 		form := forms.CreateUserForm{
-// 			Email:    email,
-// 			Password: pw,
-// 		}
-
-// 		admUser, err = form.ToUser(repo)
-
-// 		if err != nil {
-// 			return err
-// 		}
-
-// 		admUser, err = repo.CreateUser(admUser)
-
-// 		if err != nil {
-// 			return err
-// 		}
-// 	}
-
-// 	filename := "/porter/porter.kubeconfig"
-
-// 	// read if kubeconfig file exists, if it does update the user
-// 	if _, err := os.Stat(filename); !os.IsNotExist(err) {
-// 		fileBytes, err := ioutil.ReadFile(filename)
-
-// 		contexts := make([]string, 0)
-// 		allContexts, err := kubernetes.GetContextsFromBytes(fileBytes, []string{})
-
-// 		if err != nil {
-// 			return err
-// 		}
-
-// 		for _, context := range allContexts {
-// 			contexts = append(contexts, context.Name)
-// 		}
-
-// 		form := forms.UpdateUserForm{
-// 			ID:              admUser.ID,
-// 			RawKubeConfig:   string(fileBytes),
-// 			AllowedContexts: contexts,
-// 		}
-
-// 		admUser, err = form.ToUser(repo)
-
-// 		if err != nil {
-// 			return err
-// 		}
-
-// 		admUser, err = repo.UpdateUser(admUser)
-
-// 		if err != nil {
-// 			return err
-// 		}
-// 	}
-
-// 	return nil
-// }

+ 0 - 4
internal/config/config.go

@@ -42,10 +42,6 @@ type DBConf struct {
 	Password string `env:"DB_PASS,default=porter"`
 	Password string `env:"DB_PASS,default=porter"`
 	DbName   string `env:"DB_NAME,default=porter"`
 	DbName   string `env:"DB_NAME,default=porter"`
 
 
-	AdminInit     bool   `env:"ADMIN_INIT,default=true"`
-	AdminEmail    string `env:"ADMIN_EMAIL,default=admin@example.com"`
-	AdminPassword string `env:"ADMIN_PASSWORD,default=password"`
-
 	SQLLite     bool   `env:"SQL_LITE,default=false"`
 	SQLLite     bool   `env:"SQL_LITE,default=false"`
 	SQLLitePath string `env:"SQL_LITE_PATH,default=/porter/porter.db"`
 	SQLLitePath string `env:"SQL_LITE_PATH,default=/porter/porter.db"`
 }
 }

+ 8 - 0
internal/repository/gorm/project.go

@@ -64,3 +64,11 @@ func (repo *ProjectRepository) ListProjectsByUserID(userID uint) ([]*models.Proj
 
 
 	return projects, nil
 	return projects, nil
 }
 }
+
+// DeleteProject deletes a project (marking deleted in the db)
+func (repo *ProjectRepository) DeleteProject(project *models.Project) (*models.Project, error) {
+	if err := repo.db.Delete(&project).Error; err != nil {
+		return nil, err
+	}
+	return project, nil
+}

+ 24 - 0
internal/repository/gorm/project_test.go

@@ -6,6 +6,7 @@ import (
 	"github.com/go-test/deep"
 	"github.com/go-test/deep"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 
 
+	"gorm.io/gorm"
 	orm "gorm.io/gorm"
 	orm "gorm.io/gorm"
 )
 )
 
 
@@ -168,3 +169,26 @@ func TestListProjectsByUserID(t *testing.T) {
 	}
 	}
 
 
 }
 }
+
+func TestDeleteProject(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_create_proj_role.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	defer cleanup(tester, t)
+
+	proj, err := tester.repo.Project.DeleteProject(tester.initProjects[0])
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// attempt to read the project and ensure that the error is gorm.ErrRecordNotFound
+	_, err = tester.repo.Project.ReadProject(proj.Model.ID)
+
+	if err != gorm.ErrRecordNotFound {
+		t.Fatalf("read should have returned record not found: returned %v\n", err)
+	}
+}

+ 1 - 0
internal/repository/project.go

@@ -13,4 +13,5 @@ type ProjectRepository interface {
 	CreateProjectRole(project *models.Project, role *models.Role) (*models.Role, error)
 	CreateProjectRole(project *models.Project, role *models.Role) (*models.Role, error)
 	ReadProject(id uint) (*models.Project, error)
 	ReadProject(id uint) (*models.Project, error)
 	ListProjectsByUserID(userID uint) ([]*models.Project, error)
 	ListProjectsByUserID(userID uint) ([]*models.Project, error)
+	DeleteProject(project *models.Project) (*models.Project, error)
 }
 }

+ 16 - 0
internal/repository/test/project.go

@@ -84,3 +84,19 @@ func (repo *ProjectRepository) ListProjectsByUserID(userID uint) ([]*models.Proj
 
 
 	return resp, nil
 	return resp, nil
 }
 }
+
+// DeleteProject removes a project
+func (repo *ProjectRepository) DeleteProject(project *models.Project) (*models.Project, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	if int(project.ID-1) >= len(repo.projects) || repo.projects[project.ID-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(project.ID - 1)
+	repo.projects[index] = nil
+
+	return project, nil
+}

+ 36 - 2
server/api/project_handler.go

@@ -89,7 +89,7 @@ func (app *App) HandleReadProject(w http.ResponseWriter, r *http.Request) {
 	id, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 	id, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 
 
 	if err != nil || id == 0 {
 	if err != nil || id == 0 {
-		app.handleErrorFormDecoding(err, ErrUserDecode, w)
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
 		return
 		return
 	}
 	}
 
 
@@ -118,7 +118,7 @@ func (app *App) HandleListProjectClusters(w http.ResponseWriter, r *http.Request
 	id, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 	id, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 
 
 	if err != nil || id == 0 {
 	if err != nil || id == 0 {
-		app.handleErrorFormDecoding(err, ErrUserDecode, w)
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
 		return
 		return
 	}
 	}
 
 
@@ -387,3 +387,37 @@ func (app *App) HandleResolveSACandidateActions(w http.ResponseWriter, r *http.R
 		w.WriteHeader(http.StatusNotModified)
 		w.WriteHeader(http.StatusNotModified)
 	}
 	}
 }
 }
+
+// HandleDeleteProject deletes a project from the db, reading from the project_id
+// in the URL param
+func (app *App) HandleDeleteProject(w http.ResponseWriter, r *http.Request) {
+	id, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || id == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	proj, err := app.repo.Project.ReadProject(uint(id))
+
+	if err != nil {
+		app.handleErrorRead(err, ErrProjectDataRead, w)
+		return
+	}
+
+	proj, err = app.repo.Project.DeleteProject(proj)
+
+	if err != nil {
+		app.handleErrorRead(err, ErrProjectDataRead, w)
+		return
+	}
+
+	projExternal := proj.Externalize()
+
+	w.WriteHeader(http.StatusOK)
+
+	if err := json.NewEncoder(w).Encode(projExternal); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}

+ 23 - 0
server/api/project_handler_test.go

@@ -267,6 +267,29 @@ func TestHandleResolveProjectSACandidate(t *testing.T) {
 	testProjRequests(t, resolveProjectSACandidatesTests, true)
 	testProjRequests(t, resolveProjectSACandidatesTests, true)
 }
 }
 
 
+var deleteProjectTests = []*projTest{
+	&projTest{
+		initializers: []func(t *tester){
+			initUserDefault,
+			initProject,
+		},
+		msg:       "Delete project",
+		method:    "DELETE",
+		endpoint:  "/api/projects/1",
+		body:      ``,
+		expStatus: http.StatusOK,
+		expBody:   `{"id":1,"name":"project-test","roles":[{"id":0,"kind":"admin","user_id":1,"project_id":1}]}`,
+		useCookie: true,
+		validators: []func(c *projTest, tester *tester, t *testing.T){
+			projectModelBodyValidator,
+		},
+	},
+}
+
+func TestHandleDeleteProject(t *testing.T) {
+	testProjRequests(t, deleteProjectTests, true)
+}
+
 // ------------------------- INITIALIZERS AND VALIDATORS ------------------------- //
 // ------------------------- INITIALIZERS AND VALIDATORS ------------------------- //
 
 
 func initProject(tester *tester) {
 func initProject(tester *tester) {

+ 10 - 0
server/router/router.go

@@ -110,6 +110,16 @@ func New(
 			),
 			),
 		)
 		)
 
 
+		r.Method(
+			"DELETE",
+			"/projects/{project_id}",
+			auth.DoesUserHaveProjectAccess(
+				requestlog.NewHandler(a.HandleDeleteProject, l),
+				mw.URLParam,
+				mw.WriteAccess,
+			),
+		)
+
 		// /api/projects/{project_id}/releases routes
 		// /api/projects/{project_id}/releases routes
 		r.Method(
 		r.Method(
 			"GET",
 			"GET",