<commit_before>package api

import (
	"encoding/json"
	"fmt"
	"github.com/golang/glog"
	"github.com/nais/naisd/api/app"
	"github.com/nais/naisd/api/naisrequest"
	ver "github.com/nais/naisd/api/version"
	"github.com/nais/naisd/pkg/event"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"goji.io"
	"goji.io/pat"
	"io"
	"io/ioutil"
	"k8s.io/client-go/kubernetes"
	"net/http"
	"net/url"
	"os"
)

type deploymentEventHandler func(deployment.Event)

type Api struct {
	Clientset              kubernetes.Interface
	FasitURL               string
	ClusterSubdomain       string
	ClusterName            string
	IstioEnabled           bool
	AuthenticationEnabled  bool
	DeploymentStatusViewer DeploymentStatusViewer
	DeploymentEventHandler deploymentEventHandler
}

var (
	clientID     = os.Getenv("AZURE_AD_SERVICE_PRINCIPAL_APP_ID")
	clientSecret = os.Getenv("AZURE_AD_SERVICE_PRINCIPAL_PASSWORD")
	tenantID     = os.Getenv("AZURE_AD_SERVICE_PRINCIPAL_TENANT")
)

type AppError interface {
	error
	Code() int
}

type appError struct {
	OriginalError error
	Message       string
	StatusCode    int
}

func (e appError) Code() int {
	return e.StatusCode
}
func (e appError) Error() string {
	if e.OriginalError != nil {
		return fmt.Sprintf("%s: %s (%d)", e.Message, e.OriginalError.Error(), e.StatusCode)
	}

	return fmt.Sprintf("%s (%d)", e.Message, e.StatusCode)
}

type appHandler func(w http.ResponseWriter, r *http.Request) *appError

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if e := fn(w, r); e != nil { // e is *appError, not os.Error.
		glog.Errorf(e.Error())
		http.Error(w, e.Error(), e.StatusCode)
	}
}

var (
	requests = prometheus.NewCounterVec(
		prometheus.CounterOpts{Name: "requests", Help: "requests pr path"}, []string{"path"},
	)
	deploys = prometheus.NewCounterVec(
		prometheus.CounterOpts{Name: "deployments", Help: "deployments done by NaisD"}, []string{"nais_app"},
	)
)

func init() {
	prometheus.MustRegister(requests)
	prometheus.MustRegister(deploys)
}

func (api Api) Handler() http.Handler {
	mux := goji.NewMux()

	mux.Handle(pat.Get("/isalive"), appHandler(api.isAlive))
	mux.Handle(pat.Post("/deploy"), appHandler(api.deploy))
	mux.Handle(pat.Get("/metrics"), promhttp.Handler())
	mux.Handle(pat.Get("/version"), appHandler(api.version))
	mux.Handle(pat.Get("/deploystatus/:namespace/:deployName"), appHandler(api.deploymentStatusHandler))
	mux.Handle(pat.Delete("/app/:namespace/:deployName"), appHandler(api.deleteApplication))
	return mux
}

// NewAPI returns a new nais daemon.
func NewAPI(clientset kubernetes.Interface, fasitURL, clusterDomain, clusterName string, istioEnabled bool, authenticationEnabled bool, d DeploymentStatusViewer, deploymentEventHandler deploymentEventHandler) Api {
	return Api{
		Clientset:              clientset,
		FasitURL:               fasitURL,
		ClusterSubdomain:       clusterDomain,
		ClusterName:            clusterName,
		IstioEnabled:           istioEnabled,
		AuthenticationEnabled:  authenticationEnabled,
		DeploymentStatusViewer: d,
		DeploymentEventHandler: deploymentEventHandler,
	}
}
func authenticate(username, password string) *appError {
	authURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/token", tenantID)
	content := url.Values{}
	content.Add("grant_type", "password")
	content.Add("username", username)
	content.Add("password", password)
	content.Add("client_id", clientID)
	content.Add("client_secret", clientSecret)
	content.Add("resource", clientID)

	res, err := http.PostForm(authURL, content)
	if err != nil {
		return &appError{err, "Failed during authentication", 500}
	}
	if res.StatusCode != 200 {
		return &appError{err, "Authentication failure", 401}
	}
	return nil
}

func (api Api) deploy(w http.ResponseWriter, r *http.Request) *appError {
	requests.With(prometheus.Labels{"path": "deploy"}).Inc()
	deploymentRequest, err := unmarshalDeploymentRequest(r.Body)

	if api.AuthenticationEnabled {
		username, password, _ := r.BasicAuth()
		if clientID == "" || tenantID == "" || clientSecret == "" {
			return &appError{fmt.Errorf("missing Azure AD configuration"), "", 500}
		}
		appErr := authenticate(username, password)
		if appErr != nil {
			return appErr
		}
	}

	// Warn about deprecated fields in deploymentRequest and set default env if not set
	warnings := ensurePropertyCompatibility(&deploymentRequest)
	if len(deploymentRequest.Namespace) == 0 {
		deploymentRequest.Namespace = "app"
	}

	if len(deploymentRequest.Namespace) == 0 {
		deploymentRequest.Namespace = "default"
	}

	deploymentRequest.ClusterName = api.ClusterName

	if err != nil {
		return &appError{err, "unable to unmarshal deployment request", http.StatusBadRequest}
	}
	glog.Infof("Received deployment request: %s", deploymentRequest)

	fasit := FasitClient{api.FasitURL, deploymentRequest.FasitUsername, deploymentRequest.FasitPassword}

	glog.Infof("Starting deployment. Deploying %s:%s to %s\n", deploymentRequest.Application, deploymentRequest.Version, deploymentRequest.FasitEnvironment)

	manifest, err := GenerateManifest(deploymentRequest)
	if err != nil {
		return &appError{err, "unable to generate manifest/nais.yaml", http.StatusInternalServerError}
	}

	var fasitEnvironmentClass string
	var naisResources []NaisResource

	if !deploymentRequest.SkipFasit {
		glog.Infof("Starting deployment. Deploying %s:%s to %s\n", deploymentRequest.Application, deploymentRequest.Version, deploymentRequest.FasitEnvironment)
	} else {
		glog.Infof("Starting deployment. Deploying %s:%s. Fasit will be skipped\n", deploymentRequest.Application, deploymentRequest.Version)
	}

	if !deploymentRequest.SkipFasit {
		if hasResources(manifest) {
			if deploymentRequest.FasitEnvironment == "" {
				return &appError{err, "no fasit environment provided, but contains resources to be consumed or exposed", http.StatusInternalServerError}
			}
			if err := validateFasitRequirements(fasit, deploymentRequest.Application, deploymentRequest.FasitEnvironment); err != nil {
				return &appError{err, "validating requirements for deployment failed", http.StatusInternalServerError}
			}
			fasitEnvironmentClass, err = fasit.GetFasitEnvironmentClass(deploymentRequest.FasitEnvironment)
		}

		naisResources, err = FetchFasitResources(fasit, deploymentRequest.Application, deploymentRequest.FasitEnvironment, deploymentRequest.Zone, manifest.FasitResources.Used)
		if err != nil {
			return &appError{err, "unable to fetch fasit resources", http.StatusBadRequest}
		}
	}

	spec := app.Spec{
		Application: deploymentRequest.Application,
		Namespace:   deploymentRequest.Namespace,
		Team:        manifest.Team,
	}
	deploymentResult, err := createOrUpdateK8sResources(spec, deploymentRequest, manifest, naisResources, api.ClusterSubdomain, api.IstioEnabled, api.Clientset)
	if err != nil {
		return &appError{err, "failed while creating or updating k8s-resources", http.StatusInternalServerError}
	}

	deploys.With(prometheus.Labels{"nais_app": deploymentRequest.Application}).Inc()

	if !deploymentRequest.SkipFasit && hasResources(manifest) {
		if err := updateFasit(fasit, deploymentRequest, naisResources, manifest, createIngressHostname(deploymentRequest.Application, deploymentRequest.Namespace, api.ClusterSubdomain), fasitEnvironmentClass, deploymentRequest.FasitEnvironment, api.ClusterSubdomain); err != nil {
			return &appError{err, "failed while updating Fasit", http.StatusInternalServerError}
		}
	}

	NotifySensuAboutDeploy(spec, &deploymentRequest, &api.ClusterName)

	// Send deployment event on Kafka topic
	deploymentEvent := NewDeploymentEvent(deploymentRequest, manifest, api.ClusterName)
	api.DeploymentEventHandler(deploymentEvent)

	w.WriteHeader(200)
	w.Write(createResponse(deploymentResult, warnings))
	return nil
}

func (api Api) deploymentStatusHandler(w http.ResponseWriter, r *http.Request) *appError {
	namespace := pat.Param(r, "namespace")
	deployName := pat.Param(r, "deployName")

	status, view, err := api.DeploymentStatusViewer.DeploymentStatusView(namespace, deployName)

	if err != nil {
		return &appError{err, "deployment not found ", http.StatusNotFound}
	}

	switch status {
	case InProgress:
		w.WriteHeader(http.StatusAccepted)
	case Failed:
		w.WriteHeader(http.StatusInternalServerError)
	case Success:
		w.WriteHeader(http.StatusOK)
	}

	if b, err := json.Marshal(view); err == nil {
		w.Write(b)
	} else {
		glog.Errorf("Unable to marshal deploy status view: %+v", view)
	}

	return nil
}

func (api Api) isAlive(w http.ResponseWriter, _ *http.Request) *appError {
	requests.With(prometheus.Labels{"path": "isAlive"}).Inc()
	fmt.Fprint(w, "")
	return nil
}

func (api Api) version(w http.ResponseWriter, _ *http.Request) *appError {
	response := map[string]string{"version": ver.Version, "revision": ver.Revision}

	if err := json.NewEncoder(w).Encode(response); err != nil {
		return &appError{err, "unable to encode JSON", 500}
	}

	return nil
}

func (api Api) deleteApplication(w http.ResponseWriter, r *http.Request) *appError {
	namespace := pat.Param(r, "namespace")
	application := pat.Param(r, "deployName")

	spec := app.Spec{Application: application, Namespace: namespace}
	result, err := deleteK8sResouces(spec, api.Clientset)

	response := ""
	if len(result) > 0 {
		response = "result: \n"
		for _, res := range result {
			response += res + "\n"
		}
	}

	if err != nil {
		return &appError{err, fmt.Sprintf("there were errors when trying to delete app: %+v", response), http.StatusInternalServerError}
	}

	glog.Infof("Deleted application %s in %s\n", application, namespace)

	w.Write([]byte(response))
	w.WriteHeader(http.StatusOK)
	return nil
}

func validateFasitRequirements(fasit FasitClientAdapter, application, fasitEnvironment string) error {
	if _, err := fasit.GetFasitEnvironmentClass(fasitEnvironment); err != nil {
		glog.Errorf("Environment '%s' does not exist in Fasit", fasitEnvironment)
		return fmt.Errorf("unable to get fasit environment: %s. %s", fasitEnvironment, err)
	}
	if err := fasit.GetFasitApplication(application); err != nil {
		glog.Errorf("Application '%s' does not exist in Fasit", application)
		return fmt.Errorf("unable to get fasit application: %s. %s", application, err)
	}

	return nil
}

func hasResources(manifest NaisManifest) bool {
	if len(manifest.FasitResources.Used) == 0 && len(manifest.FasitResources.Exposed) == 0 {
		return false
	}
	return true
}

func createResponse(deploymentResult DeploymentResult, warnings []string) []byte {
	response := "result: \n"

	if deploymentResult.Deployment != nil {
		response += "- created deployment\n"
	}
	if deploymentResult.Secret != nil {
		response += "- created secret\n"
	}
	if deploymentResult.Service != nil {
		response += "- created service\n"
	}
	if deploymentResult.Ingress != nil {
		response += "- created ingress\n"
	}
	if deploymentResult.Autoscaler != nil {
		response += "- created autoscaler\n"
	}
	if deploymentResult.AlertsConfigMap != nil {
		response += "- updated alerts configmap (app-rules)\n"
	}
	if deploymentResult.Redis != nil {
		response += "- created redis\n"
	}
	if deploymentResult.RedisService != nil {
		response += "- created redis service\n"
	}
	if deploymentResult.ServiceAccount != nil {
		response += "- created serviceaccount\n"
	}
	if deploymentResult.RoleBinding != nil {
		response += "- created rolebinding\n"
	}

	if len(warnings) > 0 {
		response += "\nWarnings:\n"
		for _, warning := range warnings {
			response += fmt.Sprintf("- %s\n", warning)
		}
	}

	return []byte(response)
}

func unmarshalDeploymentRequest(body io.ReadCloser) (naisrequest.Deploy, error) {
	requestBody, err := ioutil.ReadAll(body)
	if err != nil {
		return naisrequest.Deploy{}, fmt.Errorf("could not read deployment request body %s", err)
	}

	deploymentRequest := naisrequest.Deploy{}

	if err = json.Unmarshal(requestBody, &deploymentRequest); err != nil {
		return naisrequest.Deploy{}, fmt.Errorf("could not unmarshal body %s", err)
	}

	return deploymentRequest, nil
}

func ensurePropertyCompatibility(deploy *naisrequest.Deploy) []string {
	var warnings []string

	if len(deploy.Environment) > 0 {
		warnings = append(warnings, "Specifying environment is deprecated, specify namespace instead.")
	}

	return warnings
}
<commit_msg>App er fra gammel merge conflict, vi bruker default<commit_after>package api

import (
	"encoding/json"
	"fmt"
	"github.com/golang/glog"
	"github.com/nais/naisd/api/app"
	"github.com/nais/naisd/api/naisrequest"
	ver "github.com/nais/naisd/api/version"
	"github.com/nais/naisd/pkg/event"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"goji.io"
	"goji.io/pat"
	"io"
	"io/ioutil"
	"k8s.io/client-go/kubernetes"
	"net/http"
	"net/url"
	"os"
)

type deploymentEventHandler func(deployment.Event)

type Api struct {
	Clientset              kubernetes.Interface
	FasitURL               string
	ClusterSubdomain       string
	ClusterName            string
	IstioEnabled           bool
	AuthenticationEnabled  bool
	DeploymentStatusViewer DeploymentStatusViewer
	DeploymentEventHandler deploymentEventHandler
}

var (
	clientID     = os.Getenv("AZURE_AD_SERVICE_PRINCIPAL_APP_ID")
	clientSecret = os.Getenv("AZURE_AD_SERVICE_PRINCIPAL_PASSWORD")
	tenantID     = os.Getenv("AZURE_AD_SERVICE_PRINCIPAL_TENANT")
)

type AppError interface {
	error
	Code() int
}

type appError struct {
	OriginalError error
	Message       string
	StatusCode    int
}

func (e appError) Code() int {
	return e.StatusCode
}
func (e appError) Error() string {
	if e.OriginalError != nil {
		return fmt.Sprintf("%s: %s (%d)", e.Message, e.OriginalError.Error(), e.StatusCode)
	}

	return fmt.Sprintf("%s (%d)", e.Message, e.StatusCode)
}

type appHandler func(w http.ResponseWriter, r *http.Request) *appError

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if e := fn(w, r); e != nil { // e is *appError, not os.Error.
		glog.Errorf(e.Error())
		http.Error(w, e.Error(), e.StatusCode)
	}
}

var (
	requests = prometheus.NewCounterVec(
		prometheus.CounterOpts{Name: "requests", Help: "requests pr path"}, []string{"path"},
	)
	deploys = prometheus.NewCounterVec(
		prometheus.CounterOpts{Name: "deployments", Help: "deployments done by NaisD"}, []string{"nais_app"},
	)
)

func init() {
	prometheus.MustRegister(requests)
	prometheus.MustRegister(deploys)
}

func (api Api) Handler() http.Handler {
	mux := goji.NewMux()

	mux.Handle(pat.Get("/isalive"), appHandler(api.isAlive))
	mux.Handle(pat.Post("/deploy"), appHandler(api.deploy))
	mux.Handle(pat.Get("/metrics"), promhttp.Handler())
	mux.Handle(pat.Get("/version"), appHandler(api.version))
	mux.Handle(pat.Get("/deploystatus/:namespace/:deployName"), appHandler(api.deploymentStatusHandler))
	mux.Handle(pat.Delete("/app/:namespace/:deployName"), appHandler(api.deleteApplication))
	return mux
}

// NewAPI returns a new nais daemon.
func NewAPI(clientset kubernetes.Interface, fasitURL, clusterDomain, clusterName string, istioEnabled bool, authenticationEnabled bool, d DeploymentStatusViewer, deploymentEventHandler deploymentEventHandler) Api {
	return Api{
		Clientset:              clientset,
		FasitURL:               fasitURL,
		ClusterSubdomain:       clusterDomain,
		ClusterName:            clusterName,
		IstioEnabled:           istioEnabled,
		AuthenticationEnabled:  authenticationEnabled,
		DeploymentStatusViewer: d,
		DeploymentEventHandler: deploymentEventHandler,
	}
}
func authenticate(username, password string) *appError {
	authURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/token", tenantID)
	content := url.Values{}
	content.Add("grant_type", "password")
	content.Add("username", username)
	content.Add("password", password)
	content.Add("client_id", clientID)
	content.Add("client_secret", clientSecret)
	content.Add("resource", clientID)

	res, err := http.PostForm(authURL, content)
	if err != nil {
		return &appError{err, "Failed during authentication", 500}
	}
	if res.StatusCode != 200 {
		return &appError{err, "Authentication failure", 401}
	}
	return nil
}

func (api Api) deploy(w http.ResponseWriter, r *http.Request) *appError {
	requests.With(prometheus.Labels{"path": "deploy"}).Inc()
	deploymentRequest, err := unmarshalDeploymentRequest(r.Body)

	if api.AuthenticationEnabled {
		username, password, _ := r.BasicAuth()
		if clientID == "" || tenantID == "" || clientSecret == "" {
			return &appError{fmt.Errorf("missing Azure AD configuration"), "", 500}
		}
		appErr := authenticate(username, password)
		if appErr != nil {
			return appErr
		}
	}

	// Warn about deprecated fields in deploymentRequest and set default env if not set
	warnings := ensurePropertyCompatibility(&deploymentRequest)
	if len(deploymentRequest.Namespace) == 0 {
		deploymentRequest.Namespace = "default"
	}

	deploymentRequest.ClusterName = api.ClusterName

	if err != nil {
		return &appError{err, "unable to unmarshal deployment request", http.StatusBadRequest}
	}
	glog.Infof("Received deployment request: %s", deploymentRequest)

	fasit := FasitClient{api.FasitURL, deploymentRequest.FasitUsername, deploymentRequest.FasitPassword}

	glog.Infof("Starting deployment. Deploying %s:%s to %s\n", deploymentRequest.Application, deploymentRequest.Version, deploymentRequest.FasitEnvironment)

	manifest, err := GenerateManifest(deploymentRequest)
	if err != nil {
		return &appError{err, "unable to generate manifest/nais.yaml", http.StatusInternalServerError}
	}

	var fasitEnvironmentClass string
	var naisResources []NaisResource

	if !deploymentRequest.SkipFasit {
		glog.Infof("Starting deployment. Deploying %s:%s to %s\n", deploymentRequest.Application, deploymentRequest.Version, deploymentRequest.FasitEnvironment)
	} else {
		glog.Infof("Starting deployment. Deploying %s:%s. Fasit will be skipped\n", deploymentRequest.Application, deploymentRequest.Version)
	}

	if !deploymentRequest.SkipFasit {
		if hasResources(manifest) {
			if deploymentRequest.FasitEnvironment == "" {
				return &appError{err, "no fasit environment provided, but contains resources to be consumed or exposed", http.StatusInternalServerError}
			}
			if err := validateFasitRequirements(fasit, deploymentRequest.Application, deploymentRequest.FasitEnvironment); err != nil {
				return &appError{err, "validating requirements for deployment failed", http.StatusInternalServerError}
			}
			fasitEnvironmentClass, err = fasit.GetFasitEnvironmentClass(deploymentRequest.FasitEnvironment)
		}

		naisResources, err = FetchFasitResources(fasit, deploymentRequest.Application, deploymentRequest.FasitEnvironment, deploymentRequest.Zone, manifest.FasitResources.Used)
		if err != nil {
			return &appError{err, "unable to fetch fasit resources", http.StatusBadRequest}
		}
	}

	spec := app.Spec{
		Application: deploymentRequest.Application,
		Namespace:   deploymentRequest.Namespace,
		Team:        manifest.Team,
	}
	deploymentResult, err := createOrUpdateK8sResources(spec, deploymentRequest, manifest, naisResources, api.ClusterSubdomain, api.IstioEnabled, api.Clientset)
	if err != nil {
		return &appError{err, "failed while creating or updating k8s-resources", http.StatusInternalServerError}
	}

	deploys.With(prometheus.Labels{"nais_app": deploymentRequest.Application}).Inc()

	if !deploymentRequest.SkipFasit && hasResources(manifest) {
		if err := updateFasit(fasit, deploymentRequest, naisResources, manifest, createIngressHostname(deploymentRequest.Application, deploymentRequest.Namespace, api.ClusterSubdomain), fasitEnvironmentClass, deploymentRequest.FasitEnvironment, api.ClusterSubdomain); err != nil {
			return &appError{err, "failed while updating Fasit", http.StatusInternalServerError}
		}
	}

	NotifySensuAboutDeploy(spec, &deploymentRequest, &api.ClusterName)

	// Send deployment event on Kafka topic
	deploymentEvent := NewDeploymentEvent(deploymentRequest, manifest, api.ClusterName)
	api.DeploymentEventHandler(deploymentEvent)

	w.WriteHeader(200)
	w.Write(createResponse(deploymentResult, warnings))
	return nil
}

func (api Api) deploymentStatusHandler(w http.ResponseWriter, r *http.Request) *appError {
	namespace := pat.Param(r, "namespace")
	deployName := pat.Param(r, "deployName")

	status, view, err := api.DeploymentStatusViewer.DeploymentStatusView(namespace, deployName)

	if err != nil {
		return &appError{err, "deployment not found ", http.StatusNotFound}
	}

	switch status {
	case InProgress:
		w.WriteHeader(http.StatusAccepted)
	case Failed:
		w.WriteHeader(http.StatusInternalServerError)
	case Success:
		w.WriteHeader(http.StatusOK)
	}

	if b, err := json.Marshal(view); err == nil {
		w.Write(b)
	} else {
		glog.Errorf("Unable to marshal deploy status view: %+v", view)
	}

	return nil
}

func (api Api) isAlive(w http.ResponseWriter, _ *http.Request) *appError {
	requests.With(prometheus.Labels{"path": "isAlive"}).Inc()
	fmt.Fprint(w, "")
	return nil
}

func (api Api) version(w http.ResponseWriter, _ *http.Request) *appError {
	response := map[string]string{"version": ver.Version, "revision": ver.Revision}

	if err := json.NewEncoder(w).Encode(response); err != nil {
		return &appError{err, "unable to encode JSON", 500}
	}

	return nil
}

func (api Api) deleteApplication(w http.ResponseWriter, r *http.Request) *appError {
	namespace := pat.Param(r, "namespace")
	application := pat.Param(r, "deployName")

	spec := app.Spec{Application: application, Namespace: namespace}
	result, err := deleteK8sResouces(spec, api.Clientset)

	response := ""
	if len(result) > 0 {
		response = "result: \n"
		for _, res := range result {
			response += res + "\n"
		}
	}

	if err != nil {
		return &appError{err, fmt.Sprintf("there were errors when trying to delete app: %+v", response), http.StatusInternalServerError}
	}

	glog.Infof("Deleted application %s in %s\n", application, namespace)

	w.Write([]byte(response))
	w.WriteHeader(http.StatusOK)
	return nil
}

func validateFasitRequirements(fasit FasitClientAdapter, application, fasitEnvironment string) error {
	if _, err := fasit.GetFasitEnvironmentClass(fasitEnvironment); err != nil {
		glog.Errorf("Environment '%s' does not exist in Fasit", fasitEnvironment)
		return fmt.Errorf("unable to get fasit environment: %s. %s", fasitEnvironment, err)
	}
	if err := fasit.GetFasitApplication(application); err != nil {
		glog.Errorf("Application '%s' does not exist in Fasit", application)
		return fmt.Errorf("unable to get fasit application: %s. %s", application, err)
	}

	return nil
}

func hasResources(manifest NaisManifest) bool {
	if len(manifest.FasitResources.Used) == 0 && len(manifest.FasitResources.Exposed) == 0 {
		return false
	}
	return true
}

func createResponse(deploymentResult DeploymentResult, warnings []string) []byte {
	response := "result: \n"

	if deploymentResult.Deployment != nil {
		response += "- created deployment\n"
	}
	if deploymentResult.Secret != nil {
		response += "- created secret\n"
	}
	if deploymentResult.Service != nil {
		response += "- created service\n"
	}
	if deploymentResult.Ingress != nil {
		response += "- created ingress\n"
	}
	if deploymentResult.Autoscaler != nil {
		response += "- created autoscaler\n"
	}
	if deploymentResult.AlertsConfigMap != nil {
		response += "- updated alerts configmap (app-rules)\n"
	}
	if deploymentResult.Redis != nil {
		response += "- created redis\n"
	}
	if deploymentResult.RedisService != nil {
		response += "- created redis service\n"
	}
	if deploymentResult.ServiceAccount != nil {
		response += "- created serviceaccount\n"
	}
	if deploymentResult.RoleBinding != nil {
		response += "- created rolebinding\n"
	}

	if len(warnings) > 0 {
		response += "\nWarnings:\n"
		for _, warning := range warnings {
			response += fmt.Sprintf("- %s\n", warning)
		}
	}

	return []byte(response)
}

func unmarshalDeploymentRequest(body io.ReadCloser) (naisrequest.Deploy, error) {
	requestBody, err := ioutil.ReadAll(body)
	if err != nil {
		return naisrequest.Deploy{}, fmt.Errorf("could not read deployment request body %s", err)
	}

	deploymentRequest := naisrequest.Deploy{}

	if err = json.Unmarshal(requestBody, &deploymentRequest); err != nil {
		return naisrequest.Deploy{}, fmt.Errorf("could not unmarshal body %s", err)
	}

	return deploymentRequest, nil
}

func ensurePropertyCompatibility(deploy *naisrequest.Deploy) []string {
	var warnings []string

	if len(deploy.Environment) > 0 {
		warnings = append(warnings, "Specifying environment is deprecated, specify namespace instead.")
	}

	return warnings
}
<|endoftext|>


<commit_before>package main

import (
	"net/http"

	"github.com/gorilla/websocket"
	"github.com/lexicality/vending/shared"
)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	// Disable security until I sort out the client
	CheckOrigin: func(r *http.Request) bool { return true },
}

func wsWriteLoop(conn *shared.WSConn) {

	msgChan := messageSub()
	pingChang := conn.GetPingTicker()

	var msg string
	var err error
	var ok bool
	for {
		select {
		case msg, ok = <-msgChan:
			if !ok {
				log.Info("Killing connection due to channel closure")
				return
			} else if !conn.IsOpen() {
				log.Debug("Killing write loop due to connection closure")
				return
			}

			err = conn.WriteMessage(websocket.TextMessage, []byte(msg))
			if err != nil {
				log.Errorf("Unable to send message %s: %s", msg, err)
				return
			}
		case _ = <-pingChang:
			if conn.IsTimingOut() {
				log.Info("Killing connection due to timeout")
				return
			} else if !conn.IsOpen() {
				log.Debug("Killing write loop due to connection closure")
				return
			}

			err = conn.MaybeSendPing()
			if err != nil {
				log.Errorf("Unable to send ping: %s", err)
				return
			}
		}
	}
}

func wsHandler(w http.ResponseWriter, r *http.Request) {
	log.Notice("Connection attempt begining")
	var err error

	c, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Error(err)
		w.WriteHeader(500)
		w.Write([]byte(err.Error()))
		return
	}
	var conn = shared.NewWSConn(c)
	defer c.Close()

	_ = conn.WriteMessage(websocket.TextMessage, []byte("hi!"))

	// Ignore anything the client has to say
	go conn.ReadDiscardPump()
	// Tell them all the important things we have to say
	wsWriteLoop(conn)
}
<commit_msg>mess about with connection logic rather than the actual application<commit_after>package main

import (
	"net/http"

	"net"

	"github.com/gorilla/websocket"
	"github.com/lexicality/vending/shared"
)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	// Disable security until I sort out the client
	CheckOrigin: func(r *http.Request) bool { return true },
}

func wsWriteLoop(conn *shared.WSConn) {

	msgChan := messageSub()
	pingChang := conn.GetPingTicker()

	var msg string
	var err error
	var ok bool
	for {
		err = nil
		msg = ""
		select {
		case msg, ok = <-msgChan:
			if !ok {
				log.Info("Killing connection due to channel closure")
				conn.Close()
				return
			} else if !conn.IsOpen() {
				log.Debug("Killing write loop due to connection closure")
				return
			}

			conn.SetWriteDeadline(conn.GetWriteDeadline())
			err = conn.WriteMessage(websocket.TextMessage, []byte(msg))
		case _ = <-pingChang:
			if conn.IsTimingOut() {
				log.Info("Killing connection due to ping timeout")
				conn.Close()
				return
			} else if !conn.IsOpen() {
				log.Debug("Killing write loop due to connection closure")
				return
			}

			err = conn.MaybeSendPing()
		}

		if err == nil {
			continue
		}

		// ERROR HANDLING YEAH
		switch v := err.(type) {
		case net.Error:
			if v.Timeout() {
				log.Info("Killing connection due to write timeout")
				conn.Close()
				return
			}
		case *websocket.CloseError:
			// ?
		}

		// "idk lol"
		if msg != "" {
			log.Errorf("Unable to send message %s: %s", msg, err)
		} else {
			log.Errorf("Unable to send message: %s", err)
		}
	}
}

func wsHandler(w http.ResponseWriter, r *http.Request) {
	log.Notice("Connection attempt begining")
	var err error

	c, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Error(err)
		w.WriteHeader(500)
		w.Write([]byte(err.Error()))
		return
	}
	var conn = shared.NewWSConn(c)
	defer c.Close()

	_ = conn.WriteMessage(websocket.TextMessage, []byte("hi!"))

	// Ignore anything the client has to say
	go conn.ReadDiscardPump()
	// Tell them all the important things we have to say
	wsWriteLoop(conn)
}
<|endoftext|>


<commit_before>// Defines indexing API. APIs defined here are applicable to all packages
// under `indexing`.

package api

// TODO: Define the semantics of buffer size of channels that are returned by
// the following method receiver.

// Known index types
type IndexType string

const (
	View    IndexType = "view"
	Llrb              = "llrb"
	LevelDB           = "leveldb"
	RocksDB           = "rocksdb"
	HyperDB           = "hyperdb"
	RBTree            = "rbtree"
	CBTree            = "cbtree"
)

// Inclusion controls how the boundaries values of a range are treated
type Inclusion int

const (
	Neither Inclusion = iota
	Low
	High
	Both
)

// Uniqueness, characterizes if the algorithm demands unique keys
type Uniqueness bool

const (
	Unique    Uniqueness = true
	NonUnique            = false
)

// SortOrder characterizes if the algorithm emits keys in a predictable order
type SortOrder string

const (
	Unsorted SortOrder = "none"
	Asc                = "asc"
	Desc               = "desc"
)

// Expression to be applied on the document to get the secondary key.
type ExprType string

const (
	Simple     string = "simple"
	JavaScript        = "javascript"
	N1QL              = "n1ql"
)

// Every index ever created and maintained by this package will have an
// associated index-info structure.
type IndexInfo struct {
	Name       string    `json:"name,omitempty"`       // Name of the index
	Uuid       string    `json:"uuid,omitempty"`       // unique id for every index
	Using      IndexType `json:"using,omitempty"`      // indexing algorithm
	OnExprList []string  `json:"onExprList,omitempty"` // expression list
	Bucket     string    `json:"bucket,omitempty"`     // bucket name
	IsPrimary  bool      `json:"isPrimary,omitempty"`
	Exprtype   ExprType  `json:"exprType,omitempty"`
	Engine     Finder    `json:"engine,omitempty"` // instance of index algorithm.
}

// Accuracy characterizes if the results of the index is subject to probabilistic errors.
// When an algorithm that is not Perfect is used, the caller must verify the results.
type Accuracy float64

const (
	Useless Accuracy = 0.0
	Perfect          = 1.0
)

// Complexity characterizes space and time characteristics of the algorithm
type Complexity int

const (
	O1 Complexity = iota
	Ologm
	Ologn
	Om
	Omlogm
	Omlogn
	On
	Onlogn
	Om2
	On2
	Ounknown
)

// TraitInfo is collection of traits of an algorithm. One can query the traits of an
// entire indexing algorithm, or traits of a specific operation. May change soon.
type TraitInfo struct {
	Unique     Uniqueness
	Order      SortOrder
	Accuracy   Accuracy
	AvgTime    Complexity
	AvgSpace   Complexity
	WorstTime  Complexity
	WorstSpace Complexity
}

// Key is an array of JSON objects, per encoding/json
type Key struct {
	raw     keydata
	encoded []byte //collatejson byte representation of the key
}

type keydata struct {
	keybytes Keybytes
	docid    string
}

// Value is the primary key of the relavent document
type Value struct {
	raw     valuedata
	encoded []byte
}

type valuedata struct {
	Keybytes Keybytes
	Docid    string
	Vbucket  int
	Seqno    int64
}

type Keybytes [][]byte

type Persister interface {

	//Persist a key/value pair
	InsertMutation(key Key, value Value) error

	//Delete a key/value pair by docId
	DeleteMutation(docid string) error

	//Get an existing key/value pair by key
	GetBackIndexEntry(docid string) (Key, error)

	//Close the db. Should be able to reopen after this operation
	Close() error

	//Destroy/Wipe the DB completely
	Destroy() error
}

// Algorithm is the basic capability of any index algorithm
type Finder interface {
	Name() string
	//  Purge()
	Trait(operator interface{}) TraitInfo
	Persister
}

// Counter is a class of algorithms that return total node count efficiently
type Counter interface {
	Finder
	CountTotal() (uint64, error)
}

// Exister is a class of algorithms that allow testing if a key exists in the
// index
type Exister interface {
	Finder
	Exists(key Key) bool // TODO: Should we have the `error` part of return ?
}

// Looker is a class of algorithms that allow looking up a key in an index.
// Usually, being able to look up a key means we can iterate through all keys
// too, and so that is introduced here as well.
//
// TODO: Define the semantics of buffer size of channels that are returned by
// the following method receiver.
type Looker interface {
	Exister
	Lookup(key Key) (chan Value, chan error)
	KeySet() (chan Key, chan error)
	ValueSet() (chan Value, chan error)
}

// Ranger is a class of algorithms that can extract a range of keys from the
// index.
type Ranger interface {
	Looker
	KeyRange(low, high Key, inclusion Inclusion) (chan Key, chan error, SortOrder)
	ValueRange(low, high Key, inclusion Inclusion) (chan Value, chan error, SortOrder)
}

// RangeCounter is a class of algorithms that can count a range efficiently
type RangeCounter interface {
	Finder
	CountRange(low Key, high Key, inclusion Inclusion) (uint64, error)
}

// Mutations from projector to indexer.
type Mutation struct {
	Type         string
	Indexid      string
	SecondaryKey [][]byte
	Docid        string
	Vbucket      int
	Seqno        int64
}
<commit_msg>New methods for restartability support<commit_after>// Defines indexing API. APIs defined here are applicable to all packages
// under `indexing`.

package api

// TODO: Define the semantics of buffer size of channels that are returned by
// the following method receiver.

// Known index types
type IndexType string

const (
	View    IndexType = "view"
	Llrb              = "llrb"
	LevelDB           = "leveldb"
	RocksDB           = "rocksdb"
	HyperDB           = "hyperdb"
	RBTree            = "rbtree"
	CBTree            = "cbtree"
)

// Inclusion controls how the boundaries values of a range are treated
type Inclusion int

const (
	Neither Inclusion = iota
	Low
	High
	Both
)

// Uniqueness, characterizes if the algorithm demands unique keys
type Uniqueness bool

const (
	Unique    Uniqueness = true
	NonUnique            = false
)

// SortOrder characterizes if the algorithm emits keys in a predictable order
type SortOrder string

const (
	Unsorted SortOrder = "none"
	Asc                = "asc"
	Desc               = "desc"
)

// Expression to be applied on the document to get the secondary key.
type ExprType string

const (
	Simple     string = "simple"
	JavaScript        = "javascript"
	N1QL              = "n1ql"
)

// Every index ever created and maintained by this package will have an
// associated index-info structure.
type IndexInfo struct {
	Name       string    `json:"name,omitempty"`       // Name of the index
	Uuid       string    `json:"uuid,omitempty"`       // unique id for every index
	Using      IndexType `json:"using,omitempty"`      // indexing algorithm
	OnExprList []string  `json:"onExprList,omitempty"` // expression list
	Bucket     string    `json:"bucket,omitempty"`     // bucket name
	IsPrimary  bool      `json:"isPrimary,omitempty"`
	Exprtype   ExprType  `json:"exprType,omitempty"`
	//	Engine     Finder    `json:"engine,omitempty"` // instance of index algorithm.
}

// Accuracy characterizes if the results of the index is subject to probabilistic errors.
// When an algorithm that is not Perfect is used, the caller must verify the results.
type Accuracy float64

const (
	Useless Accuracy = 0.0
	Perfect          = 1.0
)

// Complexity characterizes space and time characteristics of the algorithm
type Complexity int

const (
	O1 Complexity = iota
	Ologm
	Ologn
	Om
	Omlogm
	Omlogn
	On
	Onlogn
	Om2
	On2
	Ounknown
)

// TraitInfo is collection of traits of an algorithm. One can query the traits of an
// entire indexing algorithm, or traits of a specific operation. May change soon.
type TraitInfo struct {
	Unique     Uniqueness
	Order      SortOrder
	Accuracy   Accuracy
	AvgTime    Complexity
	AvgSpace   Complexity
	WorstTime  Complexity
	WorstSpace Complexity
}

// Key is an array of JSON objects, per encoding/json
type Key struct {
	raw     keydata
	encoded []byte //collatejson byte representation of the key
}

type keydata struct {
	keybytes Keybytes
	docid    string
}

// Value is the primary key of the relavent document
type Value struct {
	raw     valuedata
	encoded []byte
}

type valuedata struct {
	Keybytes Keybytes
	Docid    string
	Vbucket  uint
	Seqno    uint64
}

type Keybytes [][]byte

type Persister interface {

	//Persist a key/value pair
	InsertMutation(key Key, value Value) error

	//Persist meta key/value in back index
	InsertMeta(metaid string, metavalue string) error

	//Return meta value based on metaid from back index
	GetMeta(metaid string) (string, error)

	//Delete a key/value pair by docId
	DeleteMutation(docid string) error

	//Get an existing key/value pair by key
	GetBackIndexEntry(docid string) (Key, error)

	//Close the db. Should be able to reopen after this operation
	Close() error

	//Destroy/Wipe the DB completely
	Destroy() error
}

// Algorithm is the basic capability of any index algorithm
type Finder interface {
	Name() string
	//  Purge()
	Trait(operator interface{}) TraitInfo
	Persister
}

// Counter is a class of algorithms that return total node count efficiently
type Counter interface {
	Finder
	CountTotal() (uint64, error)
}

// Exister is a class of algorithms that allow testing if a key exists in the
// index
type Exister interface {
	Finder
	Exists(key Key) bool // TODO: Should we have the `error` part of return ?
}

// Looker is a class of algorithms that allow looking up a key in an index.
// Usually, being able to look up a key means we can iterate through all keys
// too, and so that is introduced here as well.
//
// TODO: Define the semantics of buffer size of channels that are returned by
// the following method receiver.
type Looker interface {
	Exister
	Lookup(key Key) (chan Value, chan error)
	KeySet() (chan Key, chan error)
	ValueSet() (chan Value, chan error)
}

// Ranger is a class of algorithms that can extract a range of keys from the
// index.
type Ranger interface {
	Looker
	KeyRange(low, high Key, inclusion Inclusion) (chan Key, chan error, SortOrder)
	ValueRange(low, high Key, inclusion Inclusion) (chan Value, chan error, SortOrder)
}

// RangeCounter is a class of algorithms that can count a range efficiently
type RangeCounter interface {
	Finder
	CountRange(low Key, high Key, inclusion Inclusion) (uint64, error)
}

// Mutations from projector to indexer.
type Mutation struct {
	Type         string
	Indexid      string
	SecondaryKey [][]byte
	Docid        string
	Vbucket      uint
	Seqno        uint64
}

//list of index UUIDs
type IndexList []string

//sequence number for each of 1024 vbuckets
type SequenceVector [1024]uint64

//map of <Index, SequenceVector>
type IndexSequenceMap map[string]SequenceVector
<|endoftext|>


<commit_before>package api

import (
	"encoding/json"
	"fmt"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"goji.io"
	"goji.io/pat"
	"gopkg.in/yaml.v2"
	"io/ioutil"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/pkg/api/errors"
	"k8s.io/client-go/pkg/api/v1"
	"net/http"
	"log"
)

type Api struct {
	Clientset kubernetes.Clientset
}

var (
	requests = prometheus.NewCounterVec(
		prometheus.CounterOpts{Name: "requests", Help:"requests pr path"}, []string{"path"},
	)
	deploys = prometheus.NewCounterVec(
		prometheus.CounterOpts{Name: "deployment", Help:"deployments done by NaisD"}, []string{"app"},
	)
)

func init() {
	prometheus.MustRegister(requests)
	prometheus.MustRegister(deploys)
}

func (api Api) NewApi() http.Handler {
	mux := goji.NewMux()

	mux.HandleFunc(pat.Get("/pods"), api.listPods)
	mux.HandleFunc(pat.Get("/hello"), api.hello)
	mux.HandleFunc(pat.Post("/deploy"), api.deploy)
	mux.HandleFunc(pat.Get("/test"), api.testing)
	mux.Handle(pat.Get("/metrics"), promhttp.Handler())

	return mux
}

func (api Api) listPods(w http.ResponseWriter, _ *http.Request) {

	requests.With(prometheus.Labels{"path": "pods"}).Inc()
	pods, err := api.Clientset.CoreV1().Pods("").List(v1.ListOptions{})
	if err != nil {
		panic(err.Error())
	}

	fmt.Printf("There are %d pods in the cluster\n", len(pods.Items))

	for _, pod := range pods.Items {
		fmt.Println(pod.Name)
	}

	output, _ := json.MarshalIndent(pods.Items, "", "    ")

	fmt.Fprint(w, string(output))
}

func (api Api) hello(w http.ResponseWriter, _ *http.Request) {
	requests.With(prometheus.Labels{"path": "hello"}).Inc()

	fmt.Fprint(w, "banan")
}

type DeploymentRequest struct {
	Application  string
	Version      string
	Environment  string
	AppConfigUrl string
}

type AppConfig struct {
	Containers []Container
}

type Port struct {
	Name       string
	TargetPort int
	Port       int
	Protocol   string
}

type Container struct {
	Name  string
	Image string
	Ports []Port
}

func (api Api) testing(w http.ResponseWriter, r *http.Request) {
	appConfig, _ := fetchManifest("http://localhost:8080/app-config.yaml")
	fmt.Println(appConfig.Containers[0].Name)
}

func fetchManifest(url string) (AppConfig, error) {

	log.Printf("Fetching manifest from URL %s\n", url)

	if response, err := http.Get(url); err != nil {
		return AppConfig{}, err
	} else {
		defer response.Body.Close()
		var appConfig AppConfig
		if body, err := ioutil.ReadAll(response.Body); err != nil {
			return AppConfig{}, err
		} else {
			yaml.Unmarshal(body, &appConfig)
			return appConfig, nil
		}
	}
}

func (api Api) deploy(w http.ResponseWriter, r *http.Request) {
	requests.With(prometheus.Labels{"path": "deploy"}).Inc()

	body, err := ioutil.ReadAll(r.Body)

	if err != nil {
		panic(err)
	}

	var deploymentRequest DeploymentRequest

	if err = json.Unmarshal(body, &deploymentRequest); err != nil {
		panic(err)
	}

	fmt.Printf("Starting deployment. Deploying %s:%s to %s\n", deploymentRequest.Application, deploymentRequest.Version, deploymentRequest.Environment)

	var appConfigUrl string

	if deploymentRequest.AppConfigUrl != "" {
		appConfigUrl = deploymentRequest.AppConfigUrl
	} else {
		appConfigUrl = fmt.Sprintf("http://nexus.adeo.no/nexus/service/local/repositories/m2internal/content/nais/%s/%s/%s", deploymentRequest.Application, deploymentRequest.Version, fmt.Sprintf("%s-%s.yaml", deploymentRequest.Application, deploymentRequest.Version))
	}

	appConfig, err := fetchManifest(appConfigUrl)

	if err != nil {
		fmt.Printf("Unable to fetch manifest %s", err)
		w.WriteHeader(500)
		w.Write([]byte(":("))
	}

	fmt.Printf("Read app-config.yaml, looks like this:%s\n", appConfig)

	if err := api.createOrUpdateService(deploymentRequest, appConfig); err != nil {
		fmt.Println(err)
	}

	if err := api.createOrUpdateDeployment(deploymentRequest, appConfig); err != nil {
		fmt.Println(err)
	}

	if err := api.createOrUpdateIngress(deploymentRequest, appConfig); err != nil {
		fmt.Println(err)
	}

	deploys.With(prometheus.Labels{"app":deploymentRequest.Application}).Inc()

	w.Write([]byte("ok\n"))
}

func (api Api) createOrUpdateService(req DeploymentRequest, appConfig AppConfig) error {
	appName := req.Application

	serviceSpec := ResourceCreator{appConfig, req}.CreateService()

	service := api.Clientset.Core().Services("default")

	svc, err := service.Get(appName)

	switch {
	case err == nil:
		serviceSpec.ObjectMeta.ResourceVersion = svc.ObjectMeta.ResourceVersion
		serviceSpec.Spec.ClusterIP = svc.Spec.ClusterIP
		_, err = service.Update(serviceSpec)
		if err != nil {
			return fmt.Errorf("failed to update service: %s", err)
		}
		fmt.Println("service updated")
	case errors.IsNotFound(err):
		_, err = service.Create(serviceSpec)
		if err != nil {
			return fmt.Errorf("failed to create service: %s", err)
		}
		fmt.Println("service created")
	default:
		return fmt.Errorf("unexpected error: %s", err)
	}

	return nil
}

func (api Api) createOrUpdateDeployment(req DeploymentRequest, appConfig AppConfig) error {
	deploymentSpec := ResourceCreator{appConfig, req}.CreateDeployment()

	// Implement deployment update-or-create semantics.
	deploy := api.Clientset.Extensions().Deployments("default")
	_, err := deploy.Update(deploymentSpec)
	switch {
	case err == nil:
		fmt.Println("deployment controller updated")
	case !errors.IsNotFound(err):
		return fmt.Errorf("could not update deployment controller: %s", err)
	default:
		_, err = deploy.Create(deploymentSpec)
		if err != nil {
			return fmt.Errorf("could not create deployment controller: %s", err)
		}
		fmt.Println("deployment controller created")
	}

	return nil
}

func (api Api) createOrUpdateIngress(req DeploymentRequest, appConfig AppConfig) error {
	ingressSpec := ResourceCreator{appConfig, req}.CreateIngress()

	// Implement deployment update-or-create semantics.
	ingress := api.Clientset.Extensions().Ingresses("default")
	_, err := ingress.Update(ingressSpec)
	switch {
	case err == nil:
		fmt.Println("ingress updated")
	case !errors.IsNotFound(err):
		return fmt.Errorf("could not update ingress: %s", err)
	default:
		_, err = ingress.Create(ingressSpec)
		if err != nil {
			return fmt.Errorf("could not create ingress: %s", err)
		}
		fmt.Println("ingress created")
	}

	return nil
}
<commit_msg>prefix timeseries with nais<commit_after>package api

import (
	"encoding/json"
	"fmt"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"goji.io"
	"goji.io/pat"
	"gopkg.in/yaml.v2"
	"io/ioutil"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/pkg/api/errors"
	"k8s.io/client-go/pkg/api/v1"
	"net/http"
	"log"
)

type Api struct {
	Clientset kubernetes.Clientset
}

var (
	requests = prometheus.NewCounterVec(
		prometheus.CounterOpts{Name: "nais.requests", Help:"requests pr path"}, []string{"path"},
	)
	deploys = prometheus.NewCounterVec(
		prometheus.CounterOpts{Name: "nais.deployment", Help:"deployments done by NaisD"}, []string{"nais.app"},
	)
)

func init() {
	prometheus.MustRegister(requests)
	prometheus.MustRegister(deploys)
}

func (api Api) NewApi() http.Handler {
	mux := goji.NewMux()

	mux.HandleFunc(pat.Get("/pods"), api.listPods)
	mux.HandleFunc(pat.Get("/hello"), api.hello)
	mux.HandleFunc(pat.Post("/deploy"), api.deploy)
	mux.HandleFunc(pat.Get("/test"), api.testing)
	mux.Handle(pat.Get("/metrics"), promhttp.Handler())

	return mux
}

func (api Api) listPods(w http.ResponseWriter, _ *http.Request) {

	requests.With(prometheus.Labels{"path": "pods"}).Inc()
	pods, err := api.Clientset.CoreV1().Pods("").List(v1.ListOptions{})
	if err != nil {
		panic(err.Error())
	}

	fmt.Printf("There are %d pods in the cluster\n", len(pods.Items))

	for _, pod := range pods.Items {
		fmt.Println(pod.Name)
	}

	output, _ := json.MarshalIndent(pods.Items, "", "    ")

	fmt.Fprint(w, string(output))
}

func (api Api) hello(w http.ResponseWriter, _ *http.Request) {
	requests.With(prometheus.Labels{"path": "hello"}).Inc()

	fmt.Fprint(w, "banan")
}

type DeploymentRequest struct {
	Application  string
	Version      string
	Environment  string
	AppConfigUrl string
}

type AppConfig struct {
	Containers []Container
}

type Port struct {
	Name       string
	TargetPort int
	Port       int
	Protocol   string
}

type Container struct {
	Name  string
	Image string
	Ports []Port
}

func (api Api) testing(w http.ResponseWriter, r *http.Request) {
	appConfig, _ := fetchManifest("http://localhost:8080/app-config.yaml")
	fmt.Println(appConfig.Containers[0].Name)
}

func fetchManifest(url string) (AppConfig, error) {

	log.Printf("Fetching manifest from URL %s\n", url)

	if response, err := http.Get(url); err != nil {
		return AppConfig{}, err
	} else {
		defer response.Body.Close()
		var appConfig AppConfig
		if body, err := ioutil.ReadAll(response.Body); err != nil {
			return AppConfig{}, err
		} else {
			yaml.Unmarshal(body, &appConfig)
			return appConfig, nil
		}
	}
}

func (api Api) deploy(w http.ResponseWriter, r *http.Request) {
	requests.With(prometheus.Labels{"path": "deploy"}).Inc()

	body, err := ioutil.ReadAll(r.Body)

	if err != nil {
		panic(err)
	}

	var deploymentRequest DeploymentRequest

	if err = json.Unmarshal(body, &deploymentRequest); err != nil {
		panic(err)
	}

	fmt.Printf("Starting deployment. Deploying %s:%s to %s\n", deploymentRequest.Application, deploymentRequest.Version, deploymentRequest.Environment)

	var appConfigUrl string

	if deploymentRequest.AppConfigUrl != "" {
		appConfigUrl = deploymentRequest.AppConfigUrl
	} else {
		appConfigUrl = fmt.Sprintf("http://nexus.adeo.no/nexus/service/local/repositories/m2internal/content/nais/%s/%s/%s", deploymentRequest.Application, deploymentRequest.Version, fmt.Sprintf("%s-%s.yaml", deploymentRequest.Application, deploymentRequest.Version))
	}

	appConfig, err := fetchManifest(appConfigUrl)

	if err != nil {
		fmt.Printf("Unable to fetch manifest %s", err)
		w.WriteHeader(500)
		w.Write([]byte(":("))
	}

	fmt.Printf("Read app-config.yaml, looks like this:%s\n", appConfig)

	if err := api.createOrUpdateService(deploymentRequest, appConfig); err != nil {
		fmt.Println(err)
	}

	if err := api.createOrUpdateDeployment(deploymentRequest, appConfig); err != nil {
		fmt.Println(err)
	}

	if err := api.createOrUpdateIngress(deploymentRequest, appConfig); err != nil {
		fmt.Println(err)
	}

	deploys.With(prometheus.Labels{"nais.app":deploymentRequest.Application}).Inc()

	w.Write([]byte("ok\n"))
}

func (api Api) createOrUpdateService(req DeploymentRequest, appConfig AppConfig) error {
	appName := req.Application

	serviceSpec := ResourceCreator{appConfig, req}.CreateService()

	service := api.Clientset.Core().Services("default")

	svc, err := service.Get(appName)

	switch {
	case err == nil:
		serviceSpec.ObjectMeta.ResourceVersion = svc.ObjectMeta.ResourceVersion
		serviceSpec.Spec.ClusterIP = svc.Spec.ClusterIP
		_, err = service.Update(serviceSpec)
		if err != nil {
			return fmt.Errorf("failed to update service: %s", err)
		}
		fmt.Println("service updated")
	case errors.IsNotFound(err):
		_, err = service.Create(serviceSpec)
		if err != nil {
			return fmt.Errorf("failed to create service: %s", err)
		}
		fmt.Println("service created")
	default:
		return fmt.Errorf("unexpected error: %s", err)
	}

	return nil
}

func (api Api) createOrUpdateDeployment(req DeploymentRequest, appConfig AppConfig) error {
	deploymentSpec := ResourceCreator{appConfig, req}.CreateDeployment()

	// Implement deployment update-or-create semantics.
	deploy := api.Clientset.Extensions().Deployments("default")
	_, err := deploy.Update(deploymentSpec)
	switch {
	case err == nil:
		fmt.Println("deployment controller updated")
	case !errors.IsNotFound(err):
		return fmt.Errorf("could not update deployment controller: %s", err)
	default:
		_, err = deploy.Create(deploymentSpec)
		if err != nil {
			return fmt.Errorf("could not create deployment controller: %s", err)
		}
		fmt.Println("deployment controller created")
	}

	return nil
}

func (api Api) createOrUpdateIngress(req DeploymentRequest, appConfig AppConfig) error {
	ingressSpec := ResourceCreator{appConfig, req}.CreateIngress()

	// Implement deployment update-or-create semantics.
	ingress := api.Clientset.Extensions().Ingresses("default")
	_, err := ingress.Update(ingressSpec)
	switch {
	case err == nil:
		fmt.Println("ingress updated")
	case !errors.IsNotFound(err):
		return fmt.Errorf("could not update ingress: %s", err)
	default:
		_, err = ingress.Create(ingressSpec)
		if err != nil {
			return fmt.Errorf("could not create ingress: %s", err)
		}
		fmt.Println("ingress created")
	}

	return nil
}
<|endoftext|>


<commit_before>package commands

var cmdSubmodule = &Command{
	Run:          submodule,
	GitExtension: true,
	Usage:        "submodule add [-p] OPTIONS [USER/]REPOSITORY DIRECTORY",
	Short:        "Initialize, update or inspect submodules",
	Long: `Submodule repository "git://github.com/USER/REPOSITORY.git" into
DIRECTORY as  with  git-submodule(1).  When  USER/  is  omitted,
assumes   your   GitHub  login.  With  -p,  use  private  remote
"git@github.com:USER/REPOSITORY.git".`,
}

func init() {
	CmdRunner.Use(cmdSubmodule)
}

/**
  $ gh submodule add jingweno/gh vendor/gh
  > git submodule add git://github.com/jingweno/gh.git vendor/gh

  $ gh submodule add -p jingweno/gh vendor/gh
  > git submodule add git@github.com:jingweno/gh.git vendor/gh

  $ gh submodule add -b gh --name gh jingweno/gh vendor/gh
  > git submodule add -b gh --name gh git://github.com/jingweno/gh.git vendor/gh
**/

func submodule(command *Command, args *Args) {
	if !args.IsParamsEmpty() {
		transformSubmoduleArgs(args)
	}
}

func transformSubmoduleArgs(args *Args) {
	var idx int
	if idx = args.IndexOfParam("add"); idx == -1 {
		return
	}
	args.RemoveParam(idx)
	transformCloneArgs(args)
	args.InsertParam(idx, "add")
}
<commit_msg>Log the submodule process.<commit_after>package commands

var cmdSubmodule = &Command{
	Run:          submodule,
	GitExtension: true,
	Usage:        "submodule add [-p] OPTIONS [USER/]REPOSITORY DIRECTORY",
	Short:        "Initialize, update or inspect submodules",
	Long: `Submodule repository "git://github.com/USER/REPOSITORY.git" into
DIRECTORY as  with  git-submodule(1).  When  USER/  is  omitted,
assumes   your   GitHub  login.  With  -p,  use  private  remote
"git@github.com:USER/REPOSITORY.git".`,
}

func init() {
	CmdRunner.Use(cmdSubmodule)
}

/**
  $ gh submodule add jingweno/gh vendor/gh
  > git submodule add git://github.com/jingweno/gh.git vendor/gh

  $ gh submodule add -p jingweno/gh vendor/gh
  > git submodule add git@github.com:jingweno/gh.git vendor/gh

  $ gh submodule add -b gh --name gh jingweno/gh vendor/gh
  > git submodule add -b gh --name gh git://github.com/jingweno/gh.git vendor/gh
**/

func submodule(command *Command, args *Args) {
	fmt.Println(args)
	if !args.IsParamsEmpty() {
		transformSubmoduleArgs(args)
	}
}

func transformSubmoduleArgs(args *Args) {
	var idx int
	if idx = args.IndexOfParam("add"); idx == -1 {
		fmt.Println("`add` not found, skipping")
		return
	}
	fmt.Println("`add` found")
	args.RemoveParam(idx)
	transformCloneArgs(args)
	args.InsertParam(idx, "add")
	fmt.Println(args)
}
<|endoftext|>


<commit_before>// Copyright 2017-2018 New Vector Ltd
// Copyright 2019-2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package sqlite3

import (
	"context"
	"database/sql"
	"encoding/json"
	"strings"

	"github.com/matrix-org/dendrite/internal"
	"github.com/matrix-org/dendrite/internal/sqlutil"
	"github.com/matrix-org/dendrite/syncapi/storage/tables"
	"github.com/matrix-org/dendrite/syncapi/types"
	"github.com/matrix-org/gomatrixserverlib"
)

const currentRoomStateSchema = `
-- Stores the current room state for every room.
CREATE TABLE IF NOT EXISTS syncapi_current_room_state (
    room_id TEXT NOT NULL,
    event_id TEXT NOT NULL,
    type TEXT NOT NULL,
    sender TEXT NOT NULL,
    contains_url BOOL NOT NULL DEFAULT false,
    state_key TEXT NOT NULL,
    headered_event_json TEXT NOT NULL,
    membership TEXT,
    added_at BIGINT,
    UNIQUE (room_id, type, state_key)
);
-- for event deletion
CREATE UNIQUE INDEX IF NOT EXISTS syncapi_event_id_idx ON syncapi_current_room_state(event_id, room_id, type, sender, contains_url);
-- for querying membership states of users
-- CREATE INDEX IF NOT EXISTS syncapi_membership_idx ON syncapi_current_room_state(type, state_key, membership) WHERE membership IS NOT NULL AND membership != 'leave';
`

const upsertRoomStateSQL = "" +
	"INSERT INTO syncapi_current_room_state (room_id, event_id, type, sender, contains_url, state_key, headered_event_json, membership, added_at)" +
	" VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)" +
	" ON CONFLICT (event_id, room_id, type, sender, contains_url)" +
	" DO UPDATE SET event_id = $2, sender=$4, contains_url=$5, headered_event_json = $7, membership = $8, added_at = $9"

const deleteRoomStateByEventIDSQL = "" +
	"DELETE FROM syncapi_current_room_state WHERE event_id = $1"

const selectRoomIDsWithMembershipSQL = "" +
	"SELECT room_id FROM syncapi_current_room_state WHERE type = 'm.room.member' AND state_key = $1 AND membership = $2"

const selectCurrentStateSQL = "" +
	"SELECT headered_event_json FROM syncapi_current_room_state WHERE room_id = $1" +
	" AND ( $2 IS NULL OR     sender IN ($2)  )" +
	" AND ( $3 IS NULL OR NOT(sender IN ($3)) )" +
	" AND ( $4 IS NULL OR     type   IN ($4)  )" +
	" AND ( $5 IS NULL OR NOT(type   IN ($5)) )" +
	" AND ( $6 IS NULL OR     contains_url = $6  )" +
	" LIMIT $7"

const selectJoinedUsersSQL = "" +
	"SELECT room_id, state_key FROM syncapi_current_room_state WHERE type = 'm.room.member' AND membership = 'join'"

const selectStateEventSQL = "" +
	"SELECT headered_event_json FROM syncapi_current_room_state WHERE room_id = $1 AND type = $2 AND state_key = $3"

const selectEventsWithEventIDsSQL = "" +
	// TODO: The session_id and transaction_id blanks are here because otherwise
	// the rowsToStreamEvents expects there to be exactly five columns. We need to
	// figure out if these really need to be in the DB, and if so, we need a
	// better permanent fix for this. - neilalexander, 2 Jan 2020
	"SELECT added_at, headered_event_json, 0 AS session_id, false AS exclude_from_sync, '' AS transaction_id" +
	" FROM syncapi_current_room_state WHERE event_id IN ($1)"

type currentRoomStateStatements struct {
	db                              *sql.DB
	writer                          *sqlutil.TransactionWriter
	streamIDStatements              *streamIDStatements
	upsertRoomStateStmt             *sql.Stmt
	deleteRoomStateByEventIDStmt    *sql.Stmt
	selectRoomIDsWithMembershipStmt *sql.Stmt
	selectCurrentStateStmt          *sql.Stmt
	selectJoinedUsersStmt           *sql.Stmt
	selectStateEventStmt            *sql.Stmt
}

func NewSqliteCurrentRoomStateTable(db *sql.DB, streamID *streamIDStatements) (tables.CurrentRoomState, error) {
	s := &currentRoomStateStatements{
		db:                 db,
		writer:             sqlutil.NewTransactionWriter(),
		streamIDStatements: streamID,
	}
	_, err := db.Exec(currentRoomStateSchema)
	if err != nil {
		return nil, err
	}
	if s.upsertRoomStateStmt, err = db.Prepare(upsertRoomStateSQL); err != nil {
		return nil, err
	}
	if s.deleteRoomStateByEventIDStmt, err = db.Prepare(deleteRoomStateByEventIDSQL); err != nil {
		return nil, err
	}
	if s.selectRoomIDsWithMembershipStmt, err = db.Prepare(selectRoomIDsWithMembershipSQL); err != nil {
		return nil, err
	}
	if s.selectCurrentStateStmt, err = db.Prepare(selectCurrentStateSQL); err != nil {
		return nil, err
	}
	if s.selectJoinedUsersStmt, err = db.Prepare(selectJoinedUsersSQL); err != nil {
		return nil, err
	}
	if s.selectStateEventStmt, err = db.Prepare(selectStateEventSQL); err != nil {
		return nil, err
	}
	return s, nil
}

// JoinedMemberLists returns a map of room ID to a list of joined user IDs.
func (s *currentRoomStateStatements) SelectJoinedUsers(
	ctx context.Context,
) (map[string][]string, error) {
	rows, err := s.selectJoinedUsersStmt.QueryContext(ctx)
	if err != nil {
		return nil, err
	}
	defer internal.CloseAndLogIfError(ctx, rows, "selectJoinedUsers: rows.close() failed")

	result := make(map[string][]string)
	for rows.Next() {
		var roomID string
		var userID string
		if err := rows.Scan(&roomID, &userID); err != nil {
			return nil, err
		}
		users := result[roomID]
		users = append(users, userID)
		result[roomID] = users
	}
	return result, nil
}

// SelectRoomIDsWithMembership returns the list of room IDs which have the given user in the given membership state.
func (s *currentRoomStateStatements) SelectRoomIDsWithMembership(
	ctx context.Context,
	txn *sql.Tx,
	userID string,
	membership string, // nolint: unparam
) ([]string, error) {
	stmt := sqlutil.TxStmt(txn, s.selectRoomIDsWithMembershipStmt)
	rows, err := stmt.QueryContext(ctx, userID, membership)
	if err != nil {
		return nil, err
	}
	defer internal.CloseAndLogIfError(ctx, rows, "selectRoomIDsWithMembership: rows.close() failed")

	var result []string
	for rows.Next() {
		var roomID string
		if err := rows.Scan(&roomID); err != nil {
			return nil, err
		}
		result = append(result, roomID)
	}
	return result, nil
}

// CurrentState returns all the current state events for the given room.
func (s *currentRoomStateStatements) SelectCurrentState(
	ctx context.Context, txn *sql.Tx, roomID string,
	stateFilterPart *gomatrixserverlib.StateFilter,
) ([]gomatrixserverlib.HeaderedEvent, error) {
	stmt := sqlutil.TxStmt(txn, s.selectCurrentStateStmt)
	rows, err := stmt.QueryContext(ctx, roomID,
		nil, // FIXME: pq.StringArray(stateFilterPart.Senders),
		nil, // FIXME: pq.StringArray(stateFilterPart.NotSenders),
		nil, // FIXME: pq.StringArray(filterConvertTypeWildcardToSQL(stateFilterPart.Types)),
		nil, // FIXME: pq.StringArray(filterConvertTypeWildcardToSQL(stateFilterPart.NotTypes)),
		stateFilterPart.ContainsURL,
		stateFilterPart.Limit,
	)
	if err != nil {
		return nil, err
	}
	defer internal.CloseAndLogIfError(ctx, rows, "selectCurrentState: rows.close() failed")

	return rowsToEvents(rows)
}

func (s *currentRoomStateStatements) DeleteRoomStateByEventID(
	ctx context.Context, txn *sql.Tx, eventID string,
) error {
	return s.writer.Do(s.db, txn, func(txn *sql.Tx) error {
		stmt := sqlutil.TxStmt(txn, s.deleteRoomStateByEventIDStmt)
		_, err := stmt.ExecContext(ctx, eventID)
		return err
	})
}

func (s *currentRoomStateStatements) UpsertRoomState(
	ctx context.Context, txn *sql.Tx,
	event gomatrixserverlib.HeaderedEvent, membership *string, addedAt types.StreamPosition,
) error {
	// Parse content as JSON and search for an "url" key
	containsURL := false
	var content map[string]interface{}
	if json.Unmarshal(event.Content(), &content) != nil {
		// Set containsURL to true if url is present
		_, containsURL = content["url"]
	}

	headeredJSON, err := json.Marshal(event)
	if err != nil {
		return err
	}

	// upsert state event
	return s.writer.Do(s.db, txn, func(txn *sql.Tx) error {
		stmt := sqlutil.TxStmt(txn, s.upsertRoomStateStmt)
		_, err := stmt.ExecContext(
			ctx,
			event.RoomID(),
			event.EventID(),
			event.Type(),
			event.Sender(),
			containsURL,
			*event.StateKey(),
			headeredJSON,
			membership,
			addedAt,
		)
		return err
	})
}

func (s *currentRoomStateStatements) SelectEventsWithEventIDs(
	ctx context.Context, txn *sql.Tx, eventIDs []string,
) ([]types.StreamEvent, error) {
	iEventIDs := make([]interface{}, len(eventIDs))
	for k, v := range eventIDs {
		iEventIDs[k] = v
	}
	query := strings.Replace(selectEventsWithEventIDsSQL, "($1)", sqlutil.QueryVariadic(len(iEventIDs)), 1)
	rows, err := txn.QueryContext(ctx, query, iEventIDs...)
	if err != nil {
		return nil, err
	}
	defer internal.CloseAndLogIfError(ctx, rows, "selectEventsWithEventIDs: rows.close() failed")
	return rowsToStreamEvents(rows)
}

func rowsToEvents(rows *sql.Rows) ([]gomatrixserverlib.HeaderedEvent, error) {
	result := []gomatrixserverlib.HeaderedEvent{}
	for rows.Next() {
		var eventBytes []byte
		if err := rows.Scan(&eventBytes); err != nil {
			return nil, err
		}
		// TODO: Handle redacted events
		var ev gomatrixserverlib.HeaderedEvent
		if err := json.Unmarshal(eventBytes, &ev); err != nil {
			return nil, err
		}
		result = append(result, ev)
	}
	return result, nil
}

func (s *currentRoomStateStatements) SelectStateEvent(
	ctx context.Context, roomID, evType, stateKey string,
) (*gomatrixserverlib.HeaderedEvent, error) {
	stmt := s.selectStateEventStmt
	var res []byte
	err := stmt.QueryRowContext(ctx, roomID, evType, stateKey).Scan(&res)
	if err == sql.ErrNoRows {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	var ev gomatrixserverlib.HeaderedEvent
	if err = json.Unmarshal(res, &ev); err != nil {
		return nil, err
	}
	return &ev, err
}
<commit_msg>Don't use more than 999 variables in SQLite querys. (#1224)<commit_after>// Copyright 2017-2018 New Vector Ltd
// Copyright 2019-2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package sqlite3

import (
	"context"
	"database/sql"
	"encoding/json"
	"strings"

	"github.com/matrix-org/dendrite/internal"
	"github.com/matrix-org/dendrite/internal/sqlutil"
	"github.com/matrix-org/dendrite/syncapi/storage/tables"
	"github.com/matrix-org/dendrite/syncapi/types"
	"github.com/matrix-org/gomatrixserverlib"
)

const currentRoomStateSchema = `
-- Stores the current room state for every room.
CREATE TABLE IF NOT EXISTS syncapi_current_room_state (
    room_id TEXT NOT NULL,
    event_id TEXT NOT NULL,
    type TEXT NOT NULL,
    sender TEXT NOT NULL,
    contains_url BOOL NOT NULL DEFAULT false,
    state_key TEXT NOT NULL,
    headered_event_json TEXT NOT NULL,
    membership TEXT,
    added_at BIGINT,
    UNIQUE (room_id, type, state_key)
);
-- for event deletion
CREATE UNIQUE INDEX IF NOT EXISTS syncapi_event_id_idx ON syncapi_current_room_state(event_id, room_id, type, sender, contains_url);
-- for querying membership states of users
-- CREATE INDEX IF NOT EXISTS syncapi_membership_idx ON syncapi_current_room_state(type, state_key, membership) WHERE membership IS NOT NULL AND membership != 'leave';
`

const upsertRoomStateSQL = "" +
	"INSERT INTO syncapi_current_room_state (room_id, event_id, type, sender, contains_url, state_key, headered_event_json, membership, added_at)" +
	" VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)" +
	" ON CONFLICT (event_id, room_id, type, sender, contains_url)" +
	" DO UPDATE SET event_id = $2, sender=$4, contains_url=$5, headered_event_json = $7, membership = $8, added_at = $9"

const deleteRoomStateByEventIDSQL = "" +
	"DELETE FROM syncapi_current_room_state WHERE event_id = $1"

const selectRoomIDsWithMembershipSQL = "" +
	"SELECT room_id FROM syncapi_current_room_state WHERE type = 'm.room.member' AND state_key = $1 AND membership = $2"

const selectCurrentStateSQL = "" +
	"SELECT headered_event_json FROM syncapi_current_room_state WHERE room_id = $1" +
	" AND ( $2 IS NULL OR     sender IN ($2)  )" +
	" AND ( $3 IS NULL OR NOT(sender IN ($3)) )" +
	" AND ( $4 IS NULL OR     type   IN ($4)  )" +
	" AND ( $5 IS NULL OR NOT(type   IN ($5)) )" +
	" AND ( $6 IS NULL OR     contains_url = $6  )" +
	" LIMIT $7"

const selectJoinedUsersSQL = "" +
	"SELECT room_id, state_key FROM syncapi_current_room_state WHERE type = 'm.room.member' AND membership = 'join'"

const selectStateEventSQL = "" +
	"SELECT headered_event_json FROM syncapi_current_room_state WHERE room_id = $1 AND type = $2 AND state_key = $3"

const selectEventsWithEventIDsSQL = "" +
	// TODO: The session_id and transaction_id blanks are here because otherwise
	// the rowsToStreamEvents expects there to be exactly five columns. We need to
	// figure out if these really need to be in the DB, and if so, we need a
	// better permanent fix for this. - neilalexander, 2 Jan 2020
	"SELECT added_at, headered_event_json, 0 AS session_id, false AS exclude_from_sync, '' AS transaction_id" +
	" FROM syncapi_current_room_state WHERE event_id IN ($1)"

type currentRoomStateStatements struct {
	db                              *sql.DB
	writer                          *sqlutil.TransactionWriter
	streamIDStatements              *streamIDStatements
	upsertRoomStateStmt             *sql.Stmt
	deleteRoomStateByEventIDStmt    *sql.Stmt
	selectRoomIDsWithMembershipStmt *sql.Stmt
	selectCurrentStateStmt          *sql.Stmt
	selectJoinedUsersStmt           *sql.Stmt
	selectStateEventStmt            *sql.Stmt
}

func NewSqliteCurrentRoomStateTable(db *sql.DB, streamID *streamIDStatements) (tables.CurrentRoomState, error) {
	s := &currentRoomStateStatements{
		db:                 db,
		writer:             sqlutil.NewTransactionWriter(),
		streamIDStatements: streamID,
	}
	_, err := db.Exec(currentRoomStateSchema)
	if err != nil {
		return nil, err
	}
	if s.upsertRoomStateStmt, err = db.Prepare(upsertRoomStateSQL); err != nil {
		return nil, err
	}
	if s.deleteRoomStateByEventIDStmt, err = db.Prepare(deleteRoomStateByEventIDSQL); err != nil {
		return nil, err
	}
	if s.selectRoomIDsWithMembershipStmt, err = db.Prepare(selectRoomIDsWithMembershipSQL); err != nil {
		return nil, err
	}
	if s.selectCurrentStateStmt, err = db.Prepare(selectCurrentStateSQL); err != nil {
		return nil, err
	}
	if s.selectJoinedUsersStmt, err = db.Prepare(selectJoinedUsersSQL); err != nil {
		return nil, err
	}
	if s.selectStateEventStmt, err = db.Prepare(selectStateEventSQL); err != nil {
		return nil, err
	}
	return s, nil
}

// JoinedMemberLists returns a map of room ID to a list of joined user IDs.
func (s *currentRoomStateStatements) SelectJoinedUsers(
	ctx context.Context,
) (map[string][]string, error) {
	rows, err := s.selectJoinedUsersStmt.QueryContext(ctx)
	if err != nil {
		return nil, err
	}
	defer internal.CloseAndLogIfError(ctx, rows, "selectJoinedUsers: rows.close() failed")

	result := make(map[string][]string)
	for rows.Next() {
		var roomID string
		var userID string
		if err := rows.Scan(&roomID, &userID); err != nil {
			return nil, err
		}
		users := result[roomID]
		users = append(users, userID)
		result[roomID] = users
	}
	return result, nil
}

// SelectRoomIDsWithMembership returns the list of room IDs which have the given user in the given membership state.
func (s *currentRoomStateStatements) SelectRoomIDsWithMembership(
	ctx context.Context,
	txn *sql.Tx,
	userID string,
	membership string, // nolint: unparam
) ([]string, error) {
	stmt := sqlutil.TxStmt(txn, s.selectRoomIDsWithMembershipStmt)
	rows, err := stmt.QueryContext(ctx, userID, membership)
	if err != nil {
		return nil, err
	}
	defer internal.CloseAndLogIfError(ctx, rows, "selectRoomIDsWithMembership: rows.close() failed")

	var result []string
	for rows.Next() {
		var roomID string
		if err := rows.Scan(&roomID); err != nil {
			return nil, err
		}
		result = append(result, roomID)
	}
	return result, nil
}

// CurrentState returns all the current state events for the given room.
func (s *currentRoomStateStatements) SelectCurrentState(
	ctx context.Context, txn *sql.Tx, roomID string,
	stateFilterPart *gomatrixserverlib.StateFilter,
) ([]gomatrixserverlib.HeaderedEvent, error) {
	stmt := sqlutil.TxStmt(txn, s.selectCurrentStateStmt)
	rows, err := stmt.QueryContext(ctx, roomID,
		nil, // FIXME: pq.StringArray(stateFilterPart.Senders),
		nil, // FIXME: pq.StringArray(stateFilterPart.NotSenders),
		nil, // FIXME: pq.StringArray(filterConvertTypeWildcardToSQL(stateFilterPart.Types)),
		nil, // FIXME: pq.StringArray(filterConvertTypeWildcardToSQL(stateFilterPart.NotTypes)),
		stateFilterPart.ContainsURL,
		stateFilterPart.Limit,
	)
	if err != nil {
		return nil, err
	}
	defer internal.CloseAndLogIfError(ctx, rows, "selectCurrentState: rows.close() failed")

	return rowsToEvents(rows)
}

func (s *currentRoomStateStatements) DeleteRoomStateByEventID(
	ctx context.Context, txn *sql.Tx, eventID string,
) error {
	return s.writer.Do(s.db, txn, func(txn *sql.Tx) error {
		stmt := sqlutil.TxStmt(txn, s.deleteRoomStateByEventIDStmt)
		_, err := stmt.ExecContext(ctx, eventID)
		return err
	})
}

func (s *currentRoomStateStatements) UpsertRoomState(
	ctx context.Context, txn *sql.Tx,
	event gomatrixserverlib.HeaderedEvent, membership *string, addedAt types.StreamPosition,
) error {
	// Parse content as JSON and search for an "url" key
	containsURL := false
	var content map[string]interface{}
	if json.Unmarshal(event.Content(), &content) != nil {
		// Set containsURL to true if url is present
		_, containsURL = content["url"]
	}

	headeredJSON, err := json.Marshal(event)
	if err != nil {
		return err
	}

	// upsert state event
	return s.writer.Do(s.db, txn, func(txn *sql.Tx) error {
		stmt := sqlutil.TxStmt(txn, s.upsertRoomStateStmt)
		_, err := stmt.ExecContext(
			ctx,
			event.RoomID(),
			event.EventID(),
			event.Type(),
			event.Sender(),
			containsURL,
			*event.StateKey(),
			headeredJSON,
			membership,
			addedAt,
		)
		return err
	})
}

func minOfInts(a, b int) int {
	if a <= b {
		return a
	}
	return b
}

func (s *currentRoomStateStatements) SelectEventsWithEventIDs(
	ctx context.Context, txn *sql.Tx, eventIDs []string,
) ([]types.StreamEvent, error) {
	iEventIDs := make([]interface{}, len(eventIDs))
	for k, v := range eventIDs {
		iEventIDs[k] = v
	}
	res := make([]types.StreamEvent, 0, len(eventIDs))
	var start int
	for start < len(eventIDs) {
		n := minOfInts(len(eventIDs)-start, 999)
		query := strings.Replace(selectEventsWithEventIDsSQL, "($1)", sqlutil.QueryVariadic(n), 1)
		rows, err := txn.QueryContext(ctx, query, iEventIDs[start:start+n]...)
		if err != nil {
			return nil, err
		}
		start = start + n
		events, err := rowsToStreamEvents(rows)
		internal.CloseAndLogIfError(ctx, rows, "selectEventsWithEventIDs: rows.close() failed")
		if err != nil {
			return nil, err
		}
		res = append(res, events...)
	}
	return res, nil
}

func rowsToEvents(rows *sql.Rows) ([]gomatrixserverlib.HeaderedEvent, error) {
	result := []gomatrixserverlib.HeaderedEvent{}
	for rows.Next() {
		var eventBytes []byte
		if err := rows.Scan(&eventBytes); err != nil {
			return nil, err
		}
		// TODO: Handle redacted events
		var ev gomatrixserverlib.HeaderedEvent
		if err := json.Unmarshal(eventBytes, &ev); err != nil {
			return nil, err
		}
		result = append(result, ev)
	}
	return result, nil
}

func (s *currentRoomStateStatements) SelectStateEvent(
	ctx context.Context, roomID, evType, stateKey string,
) (*gomatrixserverlib.HeaderedEvent, error) {
	stmt := s.selectStateEventStmt
	var res []byte
	err := stmt.QueryRowContext(ctx, roomID, evType, stateKey).Scan(&res)
	if err == sql.ErrNoRows {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	var ev gomatrixserverlib.HeaderedEvent
	if err = json.Unmarshal(res, &ev); err != nil {
		return nil, err
	}
	return &ev, err
}
<|endoftext|>


<commit_before>package application

import (
	"fmt"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/cloudfoundry/cli/cf"
	"github.com/cloudfoundry/cli/cf/api"
	"github.com/cloudfoundry/cli/cf/command_metadata"
	"github.com/cloudfoundry/cli/cf/configuration"
	"github.com/cloudfoundry/cli/cf/errors"
	"github.com/cloudfoundry/cli/cf/models"
	"github.com/cloudfoundry/cli/cf/requirements"
	"github.com/cloudfoundry/cli/cf/terminal"
	"github.com/cloudfoundry/loggregatorlib/logmessage"
	"github.com/codegangsta/cli"
)

const (
	DefaultStagingTimeout = 15 * time.Minute
	DefaultStartupTimeout = 5 * time.Minute
	DefaultPingerThrottle = 5 * time.Second
)

const LogMessageTypeStaging = "STG"

type Start struct {
	ui               terminal.UI
	config           configuration.Reader
	appDisplayer     ApplicationDisplayer
	appReq           requirements.ApplicationRequirement
	appRepo          api.ApplicationRepository
	appInstancesRepo api.AppInstancesRepository
	logRepo          api.LogsRepository

	StartupTimeout time.Duration
	StagingTimeout time.Duration
	PingerThrottle time.Duration
}

type ApplicationStarter interface {
	SetStartTimeoutInSeconds(timeout int)
	ApplicationStart(app models.Application) (updatedApp models.Application, err error)
}

func NewStart(ui terminal.UI, config configuration.Reader, appDisplayer ApplicationDisplayer, appRepo api.ApplicationRepository, appInstancesRepo api.AppInstancesRepository, logRepo api.LogsRepository) (cmd *Start) {
	cmd = new(Start)
	cmd.ui = ui
	cmd.config = config
	cmd.appDisplayer = appDisplayer
	cmd.appRepo = appRepo
	cmd.appInstancesRepo = appInstancesRepo
	cmd.logRepo = logRepo

	cmd.PingerThrottle = DefaultPingerThrottle

	if os.Getenv("CF_STAGING_TIMEOUT") != "" {
		duration, err := strconv.ParseInt(os.Getenv("CF_STAGING_TIMEOUT"), 10, 64)
		if err != nil {
			cmd.ui.Failed("invalid value for env var CF_STAGING_TIMEOUT\n%s", err)
		}
		cmd.StagingTimeout = time.Duration(duration) * time.Minute
	} else {
		cmd.StagingTimeout = DefaultStagingTimeout
	}

	if os.Getenv("CF_STARTUP_TIMEOUT") != "" {
		duration, err := strconv.ParseInt(os.Getenv("CF_STARTUP_TIMEOUT"), 10, 64)
		if err != nil {
			cmd.ui.Failed("invalid value for env var CF_STARTUP_TIMEOUT\n%s", err)
		}
		cmd.StartupTimeout = time.Duration(duration) * time.Minute
	} else {
		cmd.StartupTimeout = DefaultStartupTimeout
	}

	return
}

func (cmd *Start) Metadata() command_metadata.CommandMetadata {
	return command_metadata.CommandMetadata{
		Name:        "start",
		ShortName:   "st",
		Description: "Start an app",
		Usage:       "CF_NAME start APP",
	}
}

func (cmd *Start) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) {
	if len(c.Args()) == 0 {
		cmd.ui.FailWithUsage(c)
	}

	cmd.appReq = requirementsFactory.NewApplicationRequirement(c.Args()[0])

	reqs = []requirements.Requirement{requirementsFactory.NewLoginRequirement(), cmd.appReq}
	return
}

func (cmd *Start) Run(c *cli.Context) {
	cmd.ApplicationStart(cmd.appReq.GetApplication())
}

func (cmd *Start) ApplicationStart(app models.Application) (updatedApp models.Application, err error) {
	if app.State == "started" {
		cmd.ui.Say(terminal.WarningColor("App " + app.Name + " is already started"))
		return
	}

	stopLoggingChan := make(chan bool, 1)
	loggingStartedChan := make(chan bool)

	go cmd.tailStagingLogs(app, loggingStartedChan, stopLoggingChan)

	<-loggingStartedChan // block until we have established connection to Loggregator

	cmd.ui.Say("Starting app %s in org %s / space %s as %s...",
		terminal.EntityNameColor(app.Name),
		terminal.EntityNameColor(cmd.config.OrganizationFields().Name),
		terminal.EntityNameColor(cmd.config.SpaceFields().Name),
		terminal.EntityNameColor(cmd.config.Username()),
	)

	state := "STARTED"
	updatedApp, apiErr := cmd.appRepo.Update(app.Guid, models.AppParams{State: &state})
	if apiErr != nil {
		cmd.ui.Failed(apiErr.Error())
		return
	}

	cmd.ui.Ok()

	cmd.waitForInstancesToStage(updatedApp)
	stopLoggingChan <- true

	cmd.ui.Say("")

	cmd.waitForOneRunningInstance(updatedApp)
	cmd.ui.Say(terminal.HeaderColor("\nApp started\n"))

	cmd.appDisplayer.ShowApp(updatedApp)
	return
}

func (cmd *Start) SetStartTimeoutInSeconds(timeout int) {
	cmd.StartupTimeout = time.Duration(timeout) * time.Second
}

func simpleLogMessageOutput(logMsg *logmessage.LogMessage) (msgText string) {
	msgText = string(logMsg.GetMessage())
	reg, err := regexp.Compile("[\n\r]+$")
	if err != nil {
		return
	}
	msgText = reg.ReplaceAllString(msgText, "")
	return
}

func (cmd Start) tailStagingLogs(app models.Application, startChan chan bool, stopChan chan bool) {
	onConnect := func() {
		startChan <- true
	}

	err := cmd.logRepo.TailLogsFor(app.Guid, onConnect, func(msg *logmessage.LogMessage) {
		select {
		case <-stopChan:
			cmd.logRepo.Close()
		default:
			if msg.GetSourceName() == LogMessageTypeStaging {
				cmd.ui.Say(simpleLogMessageOutput(msg))
			}
		}
	})

	if err != nil {
		cmd.ui.Warn("Warning: error tailing logs")
		cmd.ui.Say("%s", err)
		startChan <- true
	}
}

func isStagingError(err error) bool {
	httpError, ok := err.(errors.HttpError)
	return ok && httpError.ErrorCode() == errors.APP_NOT_STAGED
}

func (cmd Start) waitForInstancesToStage(app models.Application) {
	stagingStartTime := time.Now()
	_, err := cmd.appInstancesRepo.GetInstances(app.Guid)

	for isStagingError(err) && time.Since(stagingStartTime) < cmd.StagingTimeout {
		cmd.ui.Wait(cmd.PingerThrottle)
		_, err = cmd.appInstancesRepo.GetInstances(app.Guid)
	}

	if err != nil && !isStagingError(err) {
		cmd.ui.Say("")
		cmd.ui.Failed("%s\n\nTIP: use '%s' for more information",
			err.Error(),
			terminal.CommandColor(fmt.Sprintf("%s logs %s --recent", cf.Name(), app.Name)))
	}

	return
}

func (cmd Start) waitForOneRunningInstance(app models.Application) {
	var runningCount, startingCount, flappingCount, downCount int
	startupStartTime := time.Now()

	for runningCount == 0 {
		if time.Since(startupStartTime) > cmd.StartupTimeout {
			cmd.ui.Failed(fmt.Sprintf("Start app timeout\n\nTIP: use '%s' for more information", terminal.CommandColor(fmt.Sprintf("%s logs %s --recent", cf.Name(), app.Name))))
			return
		}

		instances, apiErr := cmd.appInstancesRepo.GetInstances(app.Guid)
		if apiErr != nil {
			cmd.ui.Wait(cmd.PingerThrottle)
			continue
		}

		totalCount := len(instances)
		runningCount, startingCount, flappingCount, downCount = 0, 0, 0, 0

		for _, inst := range instances {
			switch inst.State {
			case models.InstanceRunning:
				runningCount++
			case models.InstanceStarting:
				startingCount++
			case models.InstanceFlapping:
				flappingCount++
			case models.InstanceDown:
				downCount++
			}
		}

		cmd.ui.Say(instancesDetails(startingCount, downCount, runningCount, flappingCount, totalCount))

		if flappingCount > 0 {
			cmd.ui.Failed(fmt.Sprintf("Start unsuccessful\n\nTIP: use '%s' for more information", terminal.CommandColor(fmt.Sprintf("%s logs %s --recent", cf.Name(), app.Name))))
			return
		}
	}
}

func instancesDetails(startingCount, downCount, runningCount, flappingCount, totalCount int) string {
	details := []string{fmt.Sprintf("%d of %d instances running", runningCount, totalCount)}

	if startingCount > 0 {
		details = append(details, fmt.Sprintf("%d starting", startingCount))
	}

	if downCount > 0 {
		details = append(details, fmt.Sprintf("%d down", downCount))
	}

	if flappingCount > 0 {
		details = append(details, fmt.Sprintf("%d failing", flappingCount))
	}

	return strings.Join(details, ", ")
}
<commit_msg>Extract AppStagingWatcher interface<commit_after>package application

import (
	"fmt"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/cloudfoundry/cli/cf"
	"github.com/cloudfoundry/cli/cf/api"
	"github.com/cloudfoundry/cli/cf/command_metadata"
	"github.com/cloudfoundry/cli/cf/configuration"
	"github.com/cloudfoundry/cli/cf/errors"
	"github.com/cloudfoundry/cli/cf/models"
	"github.com/cloudfoundry/cli/cf/requirements"
	"github.com/cloudfoundry/cli/cf/terminal"
	"github.com/cloudfoundry/loggregatorlib/logmessage"
	"github.com/codegangsta/cli"
)

const (
	DefaultStagingTimeout = 15 * time.Minute
	DefaultStartupTimeout = 5 * time.Minute
	DefaultPingerThrottle = 5 * time.Second
)

const LogMessageTypeStaging = "STG"

type Start struct {
	ui               terminal.UI
	config           configuration.Reader
	appDisplayer     ApplicationDisplayer
	appReq           requirements.ApplicationRequirement
	appRepo          api.ApplicationRepository
	appInstancesRepo api.AppInstancesRepository
	logRepo          api.LogsRepository

	StartupTimeout time.Duration
	StagingTimeout time.Duration
	PingerThrottle time.Duration
}

type ApplicationStarter interface {
	SetStartTimeoutInSeconds(timeout int)
	ApplicationStart(app models.Application) (updatedApp models.Application, err error)
}

type ApplicationStagingWatcher interface {
	ApplicationWatchStaging(app models.Application, startCommand func(app models.Application) (models.Application, error)) (updatedApp models.Application, err error)
}

func NewStart(ui terminal.UI, config configuration.Reader, appDisplayer ApplicationDisplayer, appRepo api.ApplicationRepository, appInstancesRepo api.AppInstancesRepository, logRepo api.LogsRepository) (cmd *Start) {
	cmd = new(Start)
	cmd.ui = ui
	cmd.config = config
	cmd.appDisplayer = appDisplayer
	cmd.appRepo = appRepo
	cmd.appInstancesRepo = appInstancesRepo
	cmd.logRepo = logRepo

	cmd.PingerThrottle = DefaultPingerThrottle

	if os.Getenv("CF_STAGING_TIMEOUT") != "" {
		duration, err := strconv.ParseInt(os.Getenv("CF_STAGING_TIMEOUT"), 10, 64)
		if err != nil {
			cmd.ui.Failed("invalid value for env var CF_STAGING_TIMEOUT\n%s", err)
		}
		cmd.StagingTimeout = time.Duration(duration) * time.Minute
	} else {
		cmd.StagingTimeout = DefaultStagingTimeout
	}

	if os.Getenv("CF_STARTUP_TIMEOUT") != "" {
		duration, err := strconv.ParseInt(os.Getenv("CF_STARTUP_TIMEOUT"), 10, 64)
		if err != nil {
			cmd.ui.Failed("invalid value for env var CF_STARTUP_TIMEOUT\n%s", err)
		}
		cmd.StartupTimeout = time.Duration(duration) * time.Minute
	} else {
		cmd.StartupTimeout = DefaultStartupTimeout
	}

	return
}

func (cmd *Start) Metadata() command_metadata.CommandMetadata {
	return command_metadata.CommandMetadata{
		Name:        "start",
		ShortName:   "st",
		Description: "Start an app",
		Usage:       "CF_NAME start APP",
	}
}

func (cmd *Start) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) {
	if len(c.Args()) == 0 {
		cmd.ui.FailWithUsage(c)
	}

	cmd.appReq = requirementsFactory.NewApplicationRequirement(c.Args()[0])

	reqs = []requirements.Requirement{requirementsFactory.NewLoginRequirement(), cmd.appReq}
	return
}

func (cmd *Start) Run(c *cli.Context) {
	cmd.ApplicationStart(cmd.appReq.GetApplication())
}

func (cmd *Start) ApplicationStart(app models.Application) (updatedApp models.Application, err error) {
	if app.State == "started" {
		cmd.ui.Say(terminal.WarningColor("App " + app.Name + " is already started"))
		return
	}

	return cmd.ApplicationWatchStaging(app, func(app models.Application) (models.Application, error) {
		cmd.ui.Say("Starting app %s in org %s / space %s as %s...",
			terminal.EntityNameColor(app.Name),
			terminal.EntityNameColor(cmd.config.OrganizationFields().Name),
			terminal.EntityNameColor(cmd.config.SpaceFields().Name),
			terminal.EntityNameColor(cmd.config.Username()),
		)

		state := "STARTED"
		return cmd.appRepo.Update(app.Guid, models.AppParams{State: &state})
	})
}

func (cmd *Start) ApplicationWatchStaging(app models.Application, start func(app models.Application) (models.Application, error)) (updatedApp models.Application, err error) {
	stopLoggingChan := make(chan bool, 1)
	loggingStartedChan := make(chan bool)

	go cmd.tailStagingLogs(app, loggingStartedChan, stopLoggingChan)

	<-loggingStartedChan // block until we have established connection to Loggregator

	updatedApp, apiErr := start(app)
	if apiErr != nil {
		cmd.ui.Failed(apiErr.Error())
		return
	}

	cmd.ui.Ok()

	cmd.waitForInstancesToStage(updatedApp)
	stopLoggingChan <- true

	cmd.ui.Say("")

	cmd.waitForOneRunningInstance(updatedApp)
	cmd.ui.Say(terminal.HeaderColor("\nApp started\n"))

	cmd.appDisplayer.ShowApp(updatedApp)
	return
}

func (cmd *Start) SetStartTimeoutInSeconds(timeout int) {
	cmd.StartupTimeout = time.Duration(timeout) * time.Second
}

func simpleLogMessageOutput(logMsg *logmessage.LogMessage) (msgText string) {
	msgText = string(logMsg.GetMessage())
	reg, err := regexp.Compile("[\n\r]+$")
	if err != nil {
		return
	}
	msgText = reg.ReplaceAllString(msgText, "")
	return
}

func (cmd Start) tailStagingLogs(app models.Application, startChan chan bool, stopChan chan bool) {
	onConnect := func() {
		startChan <- true
	}

	err := cmd.logRepo.TailLogsFor(app.Guid, onConnect, func(msg *logmessage.LogMessage) {
		select {
		case <-stopChan:
			cmd.logRepo.Close()
		default:
			if msg.GetSourceName() == LogMessageTypeStaging {
				cmd.ui.Say(simpleLogMessageOutput(msg))
			}
		}
	})

	if err != nil {
		cmd.ui.Warn("Warning: error tailing logs")
		cmd.ui.Say("%s", err)
		startChan <- true
	}
}

func isStagingError(err error) bool {
	httpError, ok := err.(errors.HttpError)
	return ok && httpError.ErrorCode() == errors.APP_NOT_STAGED
}

func (cmd Start) waitForInstancesToStage(app models.Application) {
	stagingStartTime := time.Now()
	_, err := cmd.appInstancesRepo.GetInstances(app.Guid)

	for isStagingError(err) && time.Since(stagingStartTime) < cmd.StagingTimeout {
		cmd.ui.Wait(cmd.PingerThrottle)
		_, err = cmd.appInstancesRepo.GetInstances(app.Guid)
	}

	if err != nil && !isStagingError(err) {
		cmd.ui.Say("")
		cmd.ui.Failed("%s\n\nTIP: use '%s' for more information",
			err.Error(),
			terminal.CommandColor(fmt.Sprintf("%s logs %s --recent", cf.Name(), app.Name)))
	}

	return
}

func (cmd Start) waitForOneRunningInstance(app models.Application) {
	var runningCount, startingCount, flappingCount, downCount int
	startupStartTime := time.Now()

	for runningCount == 0 {
		if time.Since(startupStartTime) > cmd.StartupTimeout {
			cmd.ui.Failed(fmt.Sprintf("Start app timeout\n\nTIP: use '%s' for more information", terminal.CommandColor(fmt.Sprintf("%s logs %s --recent", cf.Name(), app.Name))))
			return
		}

		instances, apiErr := cmd.appInstancesRepo.GetInstances(app.Guid)
		if apiErr != nil {
			cmd.ui.Wait(cmd.PingerThrottle)
			continue
		}

		totalCount := len(instances)
		runningCount, startingCount, flappingCount, downCount = 0, 0, 0, 0

		for _, inst := range instances {
			switch inst.State {
			case models.InstanceRunning:
				runningCount++
			case models.InstanceStarting:
				startingCount++
			case models.InstanceFlapping:
				flappingCount++
			case models.InstanceDown:
				downCount++
			}
		}

		cmd.ui.Say(instancesDetails(startingCount, downCount, runningCount, flappingCount, totalCount))

		if flappingCount > 0 {
			cmd.ui.Failed(fmt.Sprintf("Start unsuccessful\n\nTIP: use '%s' for more information", terminal.CommandColor(fmt.Sprintf("%s logs %s --recent", cf.Name(), app.Name))))
			return
		}
	}
}

func instancesDetails(startingCount, downCount, runningCount, flappingCount, totalCount int) string {
	details := []string{fmt.Sprintf("%d of %d instances running", runningCount, totalCount)}

	if startingCount > 0 {
		details = append(details, fmt.Sprintf("%d starting", startingCount))
	}

	if downCount > 0 {
		details = append(details, fmt.Sprintf("%d down", downCount))
	}

	if flappingCount > 0 {
		details = append(details, fmt.Sprintf("%d failing", flappingCount))
	}

	return strings.Join(details, ", ")
}
<|endoftext|>