ソースを参照

support buildpack on backend

Alexander Belanger 5 年 前
コミット
bd1c098f66

+ 2 - 1
dashboard/src/components/repo-selector/RepoList.tsx

@@ -149,10 +149,11 @@ export default class ActionConfEditor extends Component<PropsType, StateType> {
     });
   };
 
+  
   renderExpanded = () => {
     if (this.props.readOnly) {
       return (
-        <ExpandedWrapperAlt>asdffdas{this.renderRepoList()}</ExpandedWrapperAlt>
+        <ExpandedWrapperAlt>{this.renderRepoList()}</ExpandedWrapperAlt>
       );
     } else {
       return (

+ 4 - 1
internal/forms/git_action.go

@@ -10,7 +10,8 @@ type CreateGitAction struct {
 	ReleaseID      uint              `json:"release_id" form:"required"`
 	GitRepo        string            `json:"git_repo" form:"required"`
 	ImageRepoURI   string            `json:"image_repo_uri" form:"required"`
-	DockerfilePath string            `json:"dockerfile_path" form:"required"`
+	DockerfilePath string            `json:"dockerfile_path"`
+	FolderPath     string            `json:"folder_path"`
 	GitRepoID      uint              `json:"git_repo_id" form:"required"`
 	BuildEnv       map[string]string `json:"env"`
 	RegistryID     uint              `json:"registry_id"`
@@ -23,6 +24,7 @@ func (ca *CreateGitAction) ToGitActionConfig() (*models.GitActionConfig, error)
 		GitRepo:        ca.GitRepo,
 		ImageRepoURI:   ca.ImageRepoURI,
 		DockerfilePath: ca.DockerfilePath,
+		FolderPath:     ca.FolderPath,
 		GitRepoID:      ca.GitRepoID,
 	}, nil
 }
@@ -31,6 +33,7 @@ type CreateGitActionOptional struct {
 	GitRepo        string            `json:"git_repo"`
 	ImageRepoURI   string            `json:"image_repo_uri"`
 	DockerfilePath string            `json:"dockerfile_path"`
+	FolderPath     string            `json:"folder_path"`
 	GitRepoID      uint              `json:"git_repo_id"`
 	BuildEnv       map[string]string `json:"env"`
 	RegistryID     uint              `json:"registry_id"`

+ 12 - 0
internal/forms/metrics.go

@@ -0,0 +1,12 @@
+package forms
+
+import (
+	"github.com/porter-dev/porter/internal/kubernetes/prometheus"
+)
+
+// MetricsQueryForm is the form for querying pod usage metrics (cpu, memory)
+type MetricsQueryForm struct {
+	*K8sForm
+
+	*prometheus.QueryOpts
+}

+ 16 - 7
internal/integrations/ci/actions/actions.go

@@ -31,6 +31,7 @@ type GithubActions struct {
 	ReleaseName  string
 
 	DockerFilePath string
+	FolderPath     string
 	ImageRepoURL   string
 
 	defaultBranch string
@@ -115,6 +116,20 @@ type GithubActionYAML struct {
 }
 
 func (g *GithubActions) GetGithubActionYAML() ([]byte, error) {
+	gaSteps := []GithubActionYAMLStep{
+		getCheckoutCodeStep(),
+		getDownloadPorterStep(),
+		getConfigurePorterStep(g.getPorterTokenSecretName()),
+	}
+
+	if g.DockerFilePath == "" {
+		gaSteps = append(gaSteps, getBuildPackPushStep(g.getBuildEnvSecretName(), g.DockerFilePath, g.ImageRepoURL))
+	} else {
+		gaSteps = append(gaSteps, getDockerBuildPushStep(g.getBuildEnvSecretName(), g.DockerFilePath, g.ImageRepoURL))
+	}
+
+	gaSteps = append(gaSteps, deployPorterWebhookStep(g.getWebhookSecretName(), g.ImageRepoURL))
+
 	actionYAML := &GithubActionYAML{
 		On: GithubActionYAMLOnPush{
 			Push: GithubActionYAMLOnPushBranches{
@@ -127,13 +142,7 @@ func (g *GithubActions) GetGithubActionYAML() ([]byte, error) {
 		Jobs: map[string]GithubActionYAMLJob{
 			"porter-deploy": {
 				RunsOn: "ubuntu-latest",
-				Steps: []GithubActionYAMLStep{
-					getCheckoutCodeStep(),
-					getDownloadPorterStep(),
-					getConfigurePorterStep(g.getPorterTokenSecretName()),
-					getDockerBuildPushStep(g.getBuildEnvSecretName(), g.DockerFilePath, g.ImageRepoURL),
-					deployPorterWebhookStep(g.getWebhookSecretName(), g.ImageRepoURL),
-				},
+				Steps:  gaSteps,
 			},
 		},
 	}

+ 145 - 0
internal/kubernetes/prometheus/metrics.go

@@ -0,0 +1,145 @@
+package prometheus
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/client-go/kubernetes"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// returns the prometheus service name
+func GetPrometheusService(clientset kubernetes.Interface) (*v1.Service, bool, error) {
+	services, err := clientset.CoreV1().Services("").List(context.TODO(), metav1.ListOptions{
+		LabelSelector: "app=prometheus,component=server,heritage=Helm",
+	})
+
+	if err != nil {
+		return nil, false, err
+	}
+
+	if len(services.Items) == 0 {
+		return nil, false, nil
+	}
+
+	return &services.Items[0], true, nil
+}
+
+type QueryOpts struct {
+	Metric     string   `schema:"metric"`
+	ShouldSum  bool     `schema:"shouldsum"`
+	PodList    []string `schema:"pods"`
+	Namespace  string   `schema:"namespace"`
+	StartRange uint     `schema:"startrange"`
+	EndRange   uint     `schema:"endrange"`
+	Resolution string   `schema:"resolution"`
+}
+
+func QueryPrometheus(
+	clientset kubernetes.Interface,
+	service *v1.Service,
+	opts *QueryOpts,
+) ([]byte, error) {
+	if len(service.Spec.Ports) == 0 {
+		return nil, fmt.Errorf("prometheus service has no exposed ports to query")
+	}
+
+	podSelector := fmt.Sprintf(`namespace="%s",pod=~"%s",container!="POD",container!=""`, opts.Namespace, strings.Join(opts.PodList, "|"))
+	query := ""
+
+	if opts.Metric == "cpu" {
+		query = fmt.Sprintf("rate(container_cpu_usage_seconds_total{%s}[5m])", podSelector)
+	} else if opts.Metric == "memory" {
+		query = fmt.Sprintf("container_memory_usage_bytes{%s}", podSelector)
+	}
+
+	if opts.ShouldSum {
+		query = fmt.Sprintf("sum(%s)", query)
+	}
+
+	queryParams := map[string]string{
+		"query": query,
+		"start": fmt.Sprintf("%d", opts.StartRange),
+		"end":   fmt.Sprintf("%d", opts.EndRange),
+		"step":  opts.Resolution,
+	}
+
+	resp := clientset.CoreV1().Services(service.Namespace).ProxyGet(
+		"http",
+		service.Name,
+		fmt.Sprintf("%d", service.Spec.Ports[0].Port),
+		"/api/v1/query_range",
+		queryParams,
+	)
+
+	rawQuery, err := resp.DoRaw(context.TODO())
+
+	if err != nil {
+		return nil, err
+	}
+
+	return parseQuery(rawQuery, opts.Metric)
+}
+
+type promRawQuery struct {
+	Data struct {
+		Result []struct {
+			Metric struct {
+				Pod string `json:"pod,omitempty"`
+			} `json:"metric,omitempty"`
+
+			Values [][]interface{} `json:"values"`
+		} `json:"result"`
+	} `json:"data"`
+}
+
+type promParsedSingletonQueryResult struct {
+	Date   interface{} `json:"date,omitempty"`
+	CPU    interface{} `json:"cpu,omitempty"`
+	Memory interface{} `json:"memory,omitempty"`
+}
+
+type promParsedSingletonQuery struct {
+	Pod     string                           `json:"pod,omitempty"`
+	Results []promParsedSingletonQueryResult `json:"results"`
+}
+
+func parseQuery(rawQuery []byte, metric string) ([]byte, error) {
+	rawQueryObj := &promRawQuery{}
+
+	json.Unmarshal(rawQuery, rawQueryObj)
+
+	res := make([]*promParsedSingletonQuery, 0)
+
+	for _, result := range rawQueryObj.Data.Result {
+		singleton := &promParsedSingletonQuery{
+			Pod: result.Metric.Pod,
+		}
+
+		singletonResults := make([]promParsedSingletonQueryResult, 0)
+
+		for _, values := range result.Values {
+			singletonResult := &promParsedSingletonQueryResult{
+				Date: values[0],
+			}
+
+			if metric == "cpu" {
+				singletonResult.CPU = values[1]
+			} else if metric == "memory" {
+				singletonResult.Memory = values[1]
+			}
+
+			singletonResults = append(singletonResults, *singletonResult)
+		}
+
+		singleton.Results = singletonResults
+
+		res = append(res, singleton)
+	}
+
+	return json.Marshal(res)
+}

+ 9 - 2
internal/models/gitrepo.go

@@ -14,7 +14,7 @@ type GitRepo struct {
 	ProjectID uint `json:"project_id"`
 
 	// The username/organization that this repo integration is linked to
-	RepoEntity string `json:"repo_entity"`
+	RepoEntity string `json:"repo_entity" gorm:"unique"`
 
 	// The various auth mechanisms available to the integration
 	OAuthIntegrationID uint
@@ -62,7 +62,10 @@ type GitActionConfig struct {
 	GitRepoID uint `json:"git_repo_id"`
 
 	// The path to the dockerfile in the git repo
-	DockerfilePath string `json:"dockerfile_path" form:"required"`
+	DockerfilePath string `json:"dockerfile_path"`
+
+	// The build context
+	FolderPath string `json:"folder_path"`
 }
 
 // GitActionConfigExternal is an external GitActionConfig to be shared over REST
@@ -78,6 +81,9 @@ type GitActionConfigExternal struct {
 
 	// The path to the dockerfile in the git repo
 	DockerfilePath string `json:"dockerfile_path" form:"required"`
+
+	// The build context
+	FolderPath string `json:"folder_path"`
 }
 
 // Externalize generates an external GitActionConfig to be shared over REST
@@ -87,5 +93,6 @@ func (r *GitActionConfig) Externalize() *GitActionConfigExternal {
 		ImageRepoURI:   r.ImageRepoURI,
 		GitRepoID:      r.GitRepoID,
 		DockerfilePath: r.DockerfilePath,
+		FolderPath:     r.FolderPath,
 	}
 }

+ 1 - 0
server/api/git_action_handler.go

@@ -164,6 +164,7 @@ func (app *App) createGitActionFromForm(
 		ProjectID:      uint(projID),
 		ReleaseName:    name,
 		DockerFilePath: gitAction.DockerfilePath,
+		FolderPath:     gitAction.FolderPath,
 		ImageRepoURL:   gitAction.ImageRepoURI,
 		PorterToken:    encoded,
 		BuildEnv:       form.BuildEnv,

+ 71 - 0
server/api/k8s_handler.go

@@ -2,13 +2,16 @@ package api
 
 import (
 	"encoding/json"
+	"fmt"
 	"net/http"
 	"net/url"
 
 	"github.com/go-chi/chi"
+	"github.com/gorilla/schema"
 	"github.com/gorilla/websocket"
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/kubernetes"
+	"github.com/porter-dev/porter/internal/kubernetes/prometheus"
 	v1 "k8s.io/api/core/v1"
 )
 
@@ -321,3 +324,71 @@ func (app *App) HandleStreamControllerStatus(w http.ResponseWriter, r *http.Requ
 		return
 	}
 }
+
+func (app *App) HandleGetPodMetrics(w http.ResponseWriter, r *http.Request) {
+	vals, err := url.ParseQuery(r.URL.RawQuery)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
+	// get the filter options
+	form := &forms.MetricsQueryForm{
+		K8sForm: &forms.K8sForm{
+			OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
+			},
+		},
+		QueryOpts: &prometheus.QueryOpts{},
+	}
+
+	form.K8sForm.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
+
+	// decode from JSON to form value
+	decoder := schema.NewDecoder()
+	decoder.IgnoreUnknownKeys(true)
+
+	if err := decoder.Decode(form.QueryOpts, vals); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrK8sValidate, w)
+		return
+	}
+
+	// create a new agent
+	var agent *kubernetes.Agent
+
+	if app.ServerConf.IsTesting {
+		agent = app.TestAgents.K8sAgent
+	} else {
+		agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
+	}
+
+	// get prometheus service
+	promSvc, found, err := prometheus.GetPrometheusService(agent.Clientset)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	if !found {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	rawQuery, err := prometheus.QueryPrometheus(agent.Clientset, promSvc, form.QueryOpts)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	fmt.Fprint(w, string(rawQuery))
+}

+ 14 - 0
server/router/router.go

@@ -1050,6 +1050,20 @@ func New(a *api.App) *chi.Mux {
 			),
 		)
 
+		r.Method(
+			"GET",
+			"/projects/{project_id}/k8s/metrics",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveClusterAccess(
+					requestlog.NewHandler(a.HandleGetPodMetrics, l),
+					mw.URLParam,
+					mw.QueryParam,
+				),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
+
 		r.Method(
 			"GET",
 			"/projects/{project_id}/k8s/{namespace}/pod/{name}/logs",