Bläddra i källkod

Merge branch 'master' of https://github.com/porter-dev/porter into beta.3.integration-frontend

jusrhee 5 år sedan
förälder
incheckning
0d4751f1e3

+ 1 - 0
cmd/migrate/main.go

@@ -28,6 +28,7 @@ func main() {
 		&models.Project{},
 		&models.Role{},
 		&models.User{},
+		&models.Release{},
 		&models.Session{},
 		&models.GitRepo{},
 		&models.Registry{},

+ 1 - 0
dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx

@@ -53,6 +53,7 @@ export default class ChartList extends Component<PropsType, StateType> {
         this.setState({ loading: false, error: true });
       } else {
         let charts = res.data || [];
+        console.log(charts)
         this.setState({ charts }, () => {
           this.setState({ loading: false, error: false });
         });

+ 1 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -18,6 +18,7 @@ import StatusSection from './status/StatusSection';
 import ValuesWrapper from '../../../../components/values-form/ValuesWrapper';
 import ValuesForm from '../../../../components/values-form/ValuesForm';
 import SettingsSection from './SettingsSection';
+import { format } from 'util';
 
 type PropsType = {
   namespace: string,

+ 14 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -44,6 +44,8 @@ export default class SettingsSection extends Component<PropsType, StateType> {
 
   // TODO: read in set image from form context instead of config
   componentDidMount() {
+    let { currentCluster, currentProject } = this.context;
+
     let image = this.props.currentChart.config?.image;
     if (image?.repository && image.tag) {
       this.setState({ 
@@ -51,6 +53,18 @@ export default class SettingsSection extends Component<PropsType, StateType> {
         selectedTag: image.tag 
       });
     }
+
+    api.getReleaseToken('<token>', {
+      namespace: this.props.currentChart.namespace,
+      cluster_id: currentCluster.id,
+      storage: StorageType.Secret
+    }, { id: currentProject.id, name: this.props.currentChart.name }, (err: any, res: any) => {
+      if (err) {
+        console.log(err)
+      } else {
+        console.log(res.data.webhook_token)
+      }
+    });
   }
 
   redeployWithNewImage = (img: string, tag: string) => {

+ 10 - 1
dashboard/src/shared/api.tsx

@@ -120,7 +120,7 @@ const upgradeChartValues = baseApi<{
   cluster_id: number,
 }>('POST', pathParams => {
   let { id, name, cluster_id } = pathParams;
-  return `/api/projects/${id}/releases/${name}/upgrade?cluster_id=${cluster_id}`;
+  return `/api/projects/${id}/releases/${name}/upgrade/hook?cluster_id=${cluster_id}&repository=fake&commit=hash`;
 });
 
 const getTemplates = baseApi('GET', '/api/templates');
@@ -149,6 +149,14 @@ const getProjects = baseApi<{}, { id: number }>('GET', pathParams => {
   return `/api/users/${pathParams.id}/projects`;
 });
 
+const getReleaseToken = baseApi<{ 
+  namespace: string,
+  cluster_id: number,
+  storage: StorageType,
+}, { name: string, id: number }>('GET', pathParams => {
+  return `/api/projects/${pathParams.id}/releases/${pathParams.name}/webhook_token`;
+});
+
 const createProject = baseApi<{ name: string }, {}>('POST', pathParams => {
   return `/api/projects`;
 });
@@ -247,6 +255,7 @@ export default {
   getBranches,
   getBranchContents,
   getProjects,
+  getReleaseToken,
   createProject,
   deleteProject,
   deployTemplate,

+ 1 - 0
internal/forms/helper_test.go

@@ -44,6 +44,7 @@ func setupTestEnv(tester *tester, t *testing.T) {
 	err = db.AutoMigrate(
 		&models.Project{},
 		&models.Role{},
+		&models.Release{},
 		&models.User{},
 		&models.Session{},
 		&models.GitRepo{},

+ 1 - 1
internal/helm/config.go

@@ -21,7 +21,7 @@ import (
 type Form struct {
 	Cluster   *models.Cluster `form:"required"`
 	Repo      *repository.Repository
-	Storage   string `json:"storage" form:"oneof=secret configmap memory"`
+	Storage   string `json:"storage" form:"oneof=secret configmap memory" default:"secret"`
 	Namespace string `json:"namespace"`
 }
 

+ 33 - 0
internal/models/release.go

@@ -0,0 +1,33 @@
+package models
+
+import (
+	"gorm.io/gorm"
+)
+
+// Release model used to retrieve webhook tokens for a chart.
+
+// Release type that extends gorm.Model
+type Release struct {
+	gorm.Model
+
+	WebhookToken string `json:"webhook_token" gorm:"unique"`
+	ClusterID    uint   `json:"cluster_id"`
+	ProjectID    uint   `json:"project_id"`
+	Name         string `json:"name"`
+	Namespace    string `json:"namespace"`
+}
+
+// ReleaseExternal represents the Release type that is sent over REST
+type ReleaseExternal struct {
+	ID uint `json:"id"`
+
+	WebhookToken string `json:"webhook_token"`
+}
+
+// Externalize generates an external User to be shared over REST
+func (r *Release) Externalize() *ReleaseExternal {
+	return &ReleaseExternal{
+		ID:           r.ID,
+		WebhookToken: r.WebhookToken,
+	}
+}

+ 1 - 0
internal/repository/gorm/helpers_test.go

@@ -51,6 +51,7 @@ func setupTestEnv(tester *tester, t *testing.T) {
 		&models.Session{},
 		&models.GitRepo{},
 		&models.Registry{},
+		&models.Release{},
 		&models.HelmRepo{},
 		&models.Cluster{},
 		&models.ClusterCandidate{},

+ 61 - 0
internal/repository/gorm/release.go

@@ -0,0 +1,61 @@
+package gorm
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// ReleaseRepository uses gorm.DB for querying the database
+type ReleaseRepository struct {
+	db *gorm.DB
+}
+
+// NewReleaseRepository returns a DefaultReleaseRepository which uses
+// gorm.DB for querying the database
+func NewReleaseRepository(db *gorm.DB) repository.ReleaseRepository {
+	return &ReleaseRepository{db}
+}
+
+// CreateRelease adds a new Release row to the Releases table in the database
+func (repo *ReleaseRepository) CreateRelease(release *models.Release) (*models.Release, error) {
+	if err := repo.db.Create(release).Error; err != nil {
+		return nil, err
+	}
+	return release, nil
+}
+
+// ReadRelease finds a single release based on their unique name and namespace pair.
+func (repo *ReleaseRepository) ReadRelease(name string, namespace string) (*models.Release, error) {
+	release := &models.Release{}
+	if err := repo.db.Where("name = ?", name).Where("namespace = ?", namespace).First(&release).Error; err != nil {
+		return nil, err
+	}
+	return release, nil
+}
+
+// ReadReleaseByWebhookToken finds a single release based on their unique webhook token.
+func (repo *ReleaseRepository) ReadReleaseByWebhookToken(token string) (*models.Release, error) {
+	release := &models.Release{}
+	if err := repo.db.Where("webhook_token = ?", token).First(&release).Error; err != nil {
+		return nil, err
+	}
+	return release, nil
+}
+
+// UpdateRelease modifies an existing Release in the database
+func (repo *ReleaseRepository) UpdateRelease(release *models.Release) (*models.Release, error) {
+	if err := repo.db.Save(release).Error; err != nil {
+		return nil, err
+	}
+
+	return release, nil
+}
+
+// DeleteRelease deletes a single user using their unique name and namespace pair
+func (repo *ReleaseRepository) DeleteRelease(release *models.Release) (*models.Release, error) {
+	if err := repo.db.Delete(&release).Error; err != nil {
+		return nil, err
+	}
+	return release, nil
+}

+ 97 - 0
internal/repository/gorm/release_test.go

@@ -0,0 +1,97 @@
+package gorm_test
+
+import (
+	"testing"
+
+	"github.com/porter-dev/porter/internal/models"
+	orm "gorm.io/gorm"
+)
+
+func TestCreateRelease(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_create_release.db",
+	}
+
+	setupTestEnv(tester, t)
+	defer cleanup(tester, t)
+
+	release := &models.Release{
+		Name:         "denver-meister-dakota",
+		Namespace:    "default",
+		ProjectID:    1,
+		ClusterID:    1,
+		WebhookToken: "abcdefgh",
+	}
+
+	release, err := tester.repo.Release.CreateRelease(release)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	release, err = tester.repo.Release.ReadRelease(release.Name, release.Namespace)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure id and name are correct"
+	if release.Model.ID != 1 {
+		t.Errorf("incorrect release ID: expected %d, got %d\n", 1, release.Model.ID)
+	}
+
+	if release.Name != "denver-meister-dakota" {
+		t.Errorf("incorrect project name: expected %s, got %s\n", "denver-meister-dakota", release.Name)
+	}
+
+	if release.WebhookToken != "abcdefgh" {
+		t.Errorf("incorrect webhook token: expected %s, got %s\n", "abcdefgh", release.WebhookToken)
+	}
+
+	release, err = tester.repo.Release.ReadReleaseByWebhookToken(release.WebhookToken)
+
+	if release.Name != "denver-meister-dakota" {
+		t.Errorf("incorrect project name: expected %s, got %s\n", "denver-meister-dakota", release.Name)
+	}
+}
+
+func TestDeleteRelease(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_delete_release.db",
+	}
+
+	setupTestEnv(tester, t)
+	defer cleanup(tester, t)
+
+	release := &models.Release{
+		Name:         "denver-meister-dakota",
+		Namespace:    "default",
+		ProjectID:    1,
+		ClusterID:    1,
+		WebhookToken: "abcdefgh",
+	}
+
+	release, err := tester.repo.Release.CreateRelease(release)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	release, err = tester.repo.Release.ReadRelease(release.Name, release.Namespace)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	_, err = tester.repo.Release.DeleteRelease(release)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	_, err = tester.repo.Release.ReadRelease(release.Name, release.Namespace)
+
+	if err != orm.ErrRecordNotFound {
+		t.Fatalf("incorrect error: expected %v, got %v\n", orm.ErrRecordNotFound, err)
+	}
+}

+ 1 - 0
internal/repository/gorm/repository.go

@@ -12,6 +12,7 @@ func NewRepository(db *gorm.DB, key *[32]byte) *repository.Repository {
 		User:             NewUserRepository(db),
 		Session:          NewSessionRepository(db),
 		Project:          NewProjectRepository(db),
+		Release:          NewReleaseRepository(db),
 		GitRepo:          NewGitRepoRepository(db, key),
 		Cluster:          NewClusterRepository(db, key),
 		HelmRepo:         NewHelmRepoRepository(db, key),

+ 17 - 0
internal/repository/release.go

@@ -0,0 +1,17 @@
+package repository
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// WriteRelease is the function type for all Release write operations
+type WriteRelease func(release *models.Release) (*models.Release, error)
+
+// ReleaseRepository represents the set of queries on the Release model
+type ReleaseRepository interface {
+	CreateRelease(release *models.Release) (*models.Release, error)
+	ReadRelease(name string, namespace string) (*models.Release, error)
+	ReadReleaseByWebhookToken(token string) (*models.Release, error)
+	UpdateRelease(release *models.Release) (*models.Release, error)
+	DeleteRelease(release *models.Release) (*models.Release, error)
+}

+ 1 - 0
internal/repository/repository.go

@@ -4,6 +4,7 @@ package repository
 type Repository struct {
 	User             UserRepository
 	Project          ProjectRepository
+	Release          ReleaseRepository
 	Session          SessionRepository
 	GitRepo          GitRepoRepository
 	Cluster          ClusterRepository

+ 27 - 0
server/api/deploy_handler.go

@@ -2,6 +2,7 @@ package api
 
 import (
 	"encoding/json"
+	"math/rand"
 	"net/http"
 	"net/url"
 
@@ -9,6 +10,7 @@ import (
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/helm/loader"
+	"github.com/porter-dev/porter/internal/models"
 )
 
 // HandleDeployTemplate triggers a chart deployment from a template
@@ -92,5 +94,30 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	// generate 8 characters long webhook token.
+	const letters = "abcdefghijklmnopqrstuvwxyz"
+	token := make([]byte, 8)
+	for i := range token {
+		token[i] = letters[rand.Intn(len(letters))]
+	}
+
+	// create release with webhook token in db
+	release := &models.Release{
+		ClusterID:    form.ReleaseForm.Form.Cluster.ID,
+		ProjectID:    form.ReleaseForm.Form.Cluster.ProjectID,
+		Namespace:    form.ReleaseForm.Form.Namespace,
+		Name:         form.ChartTemplateForm.Name,
+		WebhookToken: string(token),
+	}
+
+	_, err = app.Repo.Release.CreateRelease(release)
+
+	if err != nil {
+		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
+			Code:   ErrReleaseDeploy,
+			Errors: []string{"error creating a webhook: " + err.Error()},
+		}, w)
+	}
+
 	w.WriteHeader(http.StatusOK)
 }

+ 166 - 0
server/api/release_handler.go

@@ -2,6 +2,7 @@ package api
 
 import (
 	"encoding/json"
+	"fmt"
 	"net/http"
 	"net/url"
 	"strconv"
@@ -391,6 +392,30 @@ func (app *App) HandleListReleaseHistory(w http.ResponseWriter, r *http.Request)
 	}
 }
 
+// HandleGetReleaseToken retrieves the webhook token of a specific release.
+func (app *App) HandleGetReleaseToken(w http.ResponseWriter, r *http.Request) {
+	name := chi.URLParam(r, "name")
+
+	vals, err := url.ParseQuery(r.URL.RawQuery)
+	namespace := vals["namespace"][0]
+
+	release, err := app.Repo.Release.ReadRelease(name, namespace)
+
+	if err != nil {
+		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
+			Code:   ErrReleaseReadData,
+			Errors: []string{"release not found"},
+		}, w)
+	}
+
+	releaseExt := release.Externalize()
+
+	if err := json.NewEncoder(w).Encode(releaseExt); err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+}
+
 // HandleUpgradeRelease upgrades a release with new values.yaml
 func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
 	name := chi.URLParam(r, "name")
@@ -446,6 +471,147 @@ func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
 	w.WriteHeader(http.StatusOK)
 }
 
+// HandleReleaseDeployHook upgrades a release with new image commit
+func (app *App) HandleReleaseDeployHook(w http.ResponseWriter, r *http.Request) {
+	name := chi.URLParam(r, "name")
+	vals, err := url.ParseQuery(r.URL.RawQuery)
+
+	commit := vals["commit"][0]
+	repository := vals["repository"][0]
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
+	form := &forms.UpgradeReleaseForm{
+		ReleaseForm: &forms.ReleaseForm{
+			Form: &helm.Form{
+				Repo: app.Repo,
+			},
+		},
+		Name: name,
+	}
+
+	form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
+		vals,
+		app.Repo.Cluster,
+	)
+
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrUserDecode, w)
+		return
+	}
+
+	agent, err := app.getAgentFromReleaseForm(
+		w,
+		r,
+		form.ReleaseForm,
+	)
+
+	// errors are handled in app.getAgentFromBodyParams
+	if err != nil {
+		return
+	}
+
+	image := map[string]interface{}{}
+	image["repository"] = repository
+	image["tag"] = commit
+
+	newval := map[string]interface{}{}
+	newval["image"] = image
+
+	_, err = agent.UpgradeReleaseByValues(form.Name, newval)
+
+	if err != nil {
+		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
+			Code:   ErrReleaseDeploy,
+			Errors: []string{"error upgrading release " + err.Error()},
+		}, w)
+
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+}
+
+// HandleReleaseDeployWebhook upgrades a release when a chart specific webhook is called.
+func (app *App) HandleReleaseDeployWebhook(w http.ResponseWriter, r *http.Request) {
+	token := chi.URLParam(r, "token")
+
+	// retrieve release by token
+	release, err := app.Repo.Release.ReadReleaseByWebhookToken(token)
+
+	if err != nil {
+		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
+			Code:   ErrReleaseReadData,
+			Errors: []string{"release not found with given webhook"},
+		}, w)
+
+		return
+	}
+
+	params := map[string][]string{}
+	params["cluster_id"] = []string{fmt.Sprint(release.ClusterID)}
+	params["storage"] = []string{"secret"}
+	params["namespace"] = []string{release.Namespace}
+
+	vals, err := url.ParseQuery(r.URL.RawQuery)
+
+	commit := vals["commit"][0]
+	repository := vals["repository"][0]
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
+	form := &forms.UpgradeReleaseForm{
+		ReleaseForm: &forms.ReleaseForm{
+			Form: &helm.Form{
+				Repo: app.Repo,
+			},
+		},
+		Name: release.Name,
+	}
+
+	form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
+		params,
+		app.Repo.Cluster,
+	)
+
+	agent, err := app.getAgentFromReleaseForm(
+		w,
+		r,
+		form.ReleaseForm,
+	)
+
+	// errors are handled in app.getAgentFromBodyParams
+	if err != nil {
+		return
+	}
+
+	image := map[string]interface{}{}
+	image["repository"] = repository
+	image["tag"] = commit
+
+	newval := map[string]interface{}{}
+	newval["image"] = image
+
+	_, err = agent.UpgradeReleaseByValues(form.Name, newval)
+
+	if err != nil {
+		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
+			Code:   ErrReleaseDeploy,
+			Errors: []string{"error upgrading release " + err.Error()},
+		}, w)
+
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+}
+
 // HandleRollbackRelease rolls a release back to a specified revision
 func (app *App) HandleRollbackRelease(w http.ResponseWriter, r *http.Request) {
 	name := chi.URLParam(r, "name")

+ 26 - 0
server/router/router.go

@@ -471,6 +471,20 @@ func New(a *api.App) *chi.Mux {
 			),
 		)
 
+		r.Method(
+			"GET",
+			"/projects/{project_id}/releases/{name}/webhook_token",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveClusterAccess(
+					requestlog.NewHandler(a.HandleGetReleaseToken, l),
+					mw.URLParam,
+					mw.QueryParam,
+				),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
+
 		r.Method(
 			"POST",
 			"/projects/{project_id}/releases/{name}/upgrade",
@@ -513,6 +527,18 @@ func New(a *api.App) *chi.Mux {
 			),
 		)
 
+		// r.Method(
+		// 	"POST",
+		// 	"/projects/{project_id}/releases/{name}/upgrade/hook",
+		// 	requestlog.NewHandler(a.HandleReleaseDeployHook, l),
+		// )
+
+		r.Method(
+			"POST",
+			"/webhooks/deploy/{token}",
+			requestlog.NewHandler(a.HandleReleaseDeployWebhook, l),
+		)
+
 		// /api/projects/{project_id}/repos routes
 		// r.Method(
 		// 	"GET",