create.go 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872
  1. package porter_app
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "net/http"
  9. "strconv"
  10. "strings"
  11. "time"
  12. "connectrpc.com/connect"
  13. porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
  14. "github.com/google/uuid"
  15. "github.com/porter-dev/porter/internal/kubernetes"
  16. "github.com/porter-dev/porter/internal/kubernetes/envgroup"
  17. "github.com/porter-dev/porter/internal/telemetry"
  18. "github.com/porter-dev/porter/api/server/authz"
  19. "github.com/porter-dev/porter/api/server/handlers"
  20. "github.com/porter-dev/porter/api/server/shared"
  21. "github.com/porter-dev/porter/api/server/shared/apierrors"
  22. "github.com/porter-dev/porter/api/server/shared/config"
  23. "github.com/porter-dev/porter/api/server/shared/features"
  24. "github.com/porter-dev/porter/api/server/shared/requestutils"
  25. "github.com/porter-dev/porter/api/types"
  26. utils "github.com/porter-dev/porter/api/utils/porter_app"
  27. "github.com/porter-dev/porter/internal/helm"
  28. "github.com/porter-dev/porter/internal/helm/loader"
  29. "github.com/porter-dev/porter/internal/models"
  30. "github.com/porter-dev/porter/internal/repository"
  31. "github.com/stefanmcshane/helm/pkg/chart"
  32. )
  33. type CreatePorterAppHandler struct {
  34. handlers.PorterHandlerReadWriter
  35. authz.KubernetesAgentGetter
  36. }
  37. func NewCreatePorterAppHandler(
  38. config *config.Config,
  39. decoderValidator shared.RequestDecoderValidator,
  40. writer shared.ResultWriter,
  41. ) *CreatePorterAppHandler {
  42. return &CreatePorterAppHandler{
  43. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  44. KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
  45. }
  46. }
  47. func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  48. ctx := r.Context()
  49. project, _ := ctx.Value(types.ProjectScope).(*models.Project)
  50. cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
  51. ctx, span := telemetry.NewSpan(r.Context(), "serve-create-porter-app")
  52. defer span.End()
  53. request := &types.CreatePorterAppRequest{}
  54. if ok := c.DecodeAndValidate(w, r, request); !ok {
  55. err := telemetry.Error(ctx, span, nil, "error decoding request")
  56. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  57. return
  58. }
  59. appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
  60. if reqErr != nil {
  61. err := telemetry.Error(ctx, span, reqErr, "error getting stack name from url")
  62. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  63. return
  64. }
  65. // TODO (POR-2170): Deprecate this entire endpoint in favor of v2 endpoints
  66. if project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) {
  67. porterApp, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
  68. if err != nil {
  69. err := telemetry.Error(ctx, span, err, "porter app not found in cluster")
  70. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  71. return
  72. }
  73. appInstance, err := appInstanceFromAppName(ctx, appInstanceFromAppNameInput{
  74. ProjectID: project.ID,
  75. ClusterID: cluster.ID,
  76. AppName: appName,
  77. CCPClient: c.Config().ClusterControlPlaneClient,
  78. })
  79. if err != nil {
  80. err := telemetry.Error(ctx, span, err, "error getting deployment target id from app name")
  81. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  82. return
  83. }
  84. updateAppImageReq := connect.NewRequest(&porterv1.UpdateAppImageRequest{
  85. ProjectId: int64(project.ID),
  86. AppName: appName,
  87. RepositoryUrl: request.ImageInfo.Repository,
  88. Tag: request.ImageInfo.Tag,
  89. DeploymentTargetIdentifier: &porterv1.DeploymentTargetIdentifier{
  90. Id: appInstance.DeploymentTargetId,
  91. },
  92. })
  93. appImageResp, err := c.Config().ClusterControlPlaneClient.UpdateAppImage(ctx, updateAppImageReq)
  94. if err != nil {
  95. err := telemetry.Error(ctx, span, err, "error updating app image")
  96. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  97. return
  98. }
  99. if appImageResp == nil || appImageResp.Msg == nil {
  100. err := telemetry.Error(ctx, span, errors.New("app image response is nil"), "error updating app image")
  101. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  102. return
  103. }
  104. namespace := fmt.Sprintf("app-%s", appName)
  105. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "application-name", Value: appName})
  106. k8sAgent, err := c.GetAgent(r, cluster, namespace)
  107. if err != nil {
  108. err = telemetry.Error(ctx, span, err, "error getting k8s agent")
  109. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  110. return
  111. }
  112. revisionNumber, err := pollForRevisionNumber(ctx, pollForRevisionNumberInput{
  113. ProjectID: project.ID,
  114. RevisionID: appImageResp.Msg.RevisionId,
  115. Namespace: namespace,
  116. K8sAgent: k8sAgent,
  117. })
  118. if err != nil {
  119. err := telemetry.Error(ctx, span, err, "error polling for revision number")
  120. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  121. return
  122. }
  123. c.WriteResult(w, r, &types.PorterApp{
  124. ID: porterApp.ID,
  125. ProjectID: project.ID,
  126. ClusterID: cluster.ID,
  127. Name: appName,
  128. HelmRevisionNumber: revisionNumber,
  129. })
  130. return
  131. }
  132. namespace := utils.NamespaceFromPorterAppName(appName)
  133. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "application-name", Value: appName})
  134. helmAgent, err := c.GetHelmAgent(ctx, r, cluster, namespace)
  135. if err != nil {
  136. err = telemetry.Error(ctx, span, err, "error getting helm agent")
  137. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  138. return
  139. }
  140. k8sAgent, err := c.GetAgent(r, cluster, namespace)
  141. if err != nil {
  142. err = telemetry.Error(ctx, span, err, "error getting k8s agent")
  143. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  144. return
  145. }
  146. helmRelease, err := helmAgent.GetRelease(ctx, appName, 0, false)
  147. shouldCreate := err != nil
  148. porterYamlBase64 := request.PorterYAMLBase64
  149. porterYaml, err := base64.StdEncoding.DecodeString(porterYamlBase64)
  150. if err != nil {
  151. err = telemetry.Error(ctx, span, err, "error decoding porter yaml")
  152. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  153. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  154. return
  155. }
  156. imageInfo := request.ImageInfo
  157. registries, err := c.Repo().Registry().ListRegistriesByProjectID(cluster.ProjectID)
  158. if err != nil {
  159. err = telemetry.Error(ctx, span, err, "error listing registries")
  160. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  161. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  162. return
  163. }
  164. var releaseValues map[string]interface{}
  165. var releaseDependencies []*chart.Dependency
  166. // unless it is explicitly provided in the request, we avoid overwriting the image info
  167. // by attempting to get it from the release or the provided helm values
  168. if helmRelease != nil && (imageInfo.Repository == "" || imageInfo.Tag == "") {
  169. if request.FullHelmValues != "" {
  170. imageInfo, err = attemptToGetImageInfoFromFullHelmValues(request.FullHelmValues)
  171. if err != nil {
  172. err = telemetry.Error(ctx, span, err, "error getting image info from full helm values")
  173. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  174. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  175. return
  176. }
  177. } else {
  178. imageInfo = attemptToGetImageInfoFromRelease(helmRelease.Config)
  179. }
  180. }
  181. if shouldCreate {
  182. releaseValues = nil
  183. releaseDependencies = nil
  184. } else {
  185. releaseValues = helmRelease.Config
  186. releaseDependencies = helmRelease.Chart.Metadata.Dependencies
  187. }
  188. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "image-repo", Value: imageInfo.Repository}, telemetry.AttributeKV{Key: "image-tag", Value: imageInfo.Tag})
  189. if request.Builder == "" {
  190. // attempt to get builder from db
  191. app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
  192. if err == nil {
  193. request.Builder = app.Builder
  194. }
  195. }
  196. injectLauncher := strings.Contains(request.Builder, "heroku") ||
  197. strings.Contains(request.Builder, "paketo")
  198. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "builder", Value: request.Builder})
  199. if shouldCreate {
  200. // create the namespace if it does not exist already
  201. _, err = k8sAgent.CreateNamespace(namespace, nil)
  202. if err != nil {
  203. err = telemetry.Error(ctx, span, err, "error creating namespace")
  204. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  205. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  206. return
  207. }
  208. cloneEnvGroup(c, w, r, k8sAgent, request.EnvGroups, namespace)
  209. }
  210. if imageInfo.Repository == "" || imageInfo.Tag == "" {
  211. err = telemetry.Error(ctx, span, nil, "incomplete image info provided: must provide both repository and tag")
  212. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  213. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  214. return
  215. }
  216. var addCustomNodeSelector bool
  217. if (cluster.ProvisionedBy == "CAPI" && cluster.CloudProvider == "GCP") || cluster.GCPIntegrationID != 0 {
  218. addCustomNodeSelector = true
  219. }
  220. chart, values, preDeployJobValues, err := parse(
  221. ctx,
  222. ParseConf{
  223. PorterAppName: appName,
  224. PorterYaml: porterYaml,
  225. ImageInfo: imageInfo,
  226. ServerConfig: c.Config(),
  227. ProjectID: cluster.ProjectID,
  228. UserUpdate: request.UserUpdate,
  229. EnvGroups: request.EnvGroups,
  230. EnvironmentGroups: request.EnvironmentGroups,
  231. Namespace: namespace,
  232. ExistingHelmValues: releaseValues,
  233. ExistingChartDependencies: releaseDependencies,
  234. SubdomainCreateOpts: SubdomainCreateOpts{
  235. k8sAgent: k8sAgent,
  236. dnsRepo: c.Repo().DNSRecord(),
  237. dnsClient: c.Config().DNSClient,
  238. appRootDomain: c.Config().ServerConf.AppRootDomain,
  239. stackName: appName,
  240. },
  241. InjectLauncherToStartCommand: injectLauncher,
  242. ShouldValidateHelmValues: shouldCreate,
  243. FullHelmValues: request.FullHelmValues,
  244. AddCustomNodeSelector: addCustomNodeSelector,
  245. RemoveDeletedServices: request.OverrideRelease,
  246. },
  247. )
  248. if err != nil {
  249. err = telemetry.Error(ctx, span, err, "parse error")
  250. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  251. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  252. return
  253. }
  254. if shouldCreate {
  255. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "installing-application", Value: true})
  256. // create the release job chart if it does not exist (only done by front-end currently, where we set overrideRelease=true)
  257. if request.OverrideRelease && preDeployJobValues != nil {
  258. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "installing-pre-deploy-job", Value: true})
  259. conf, err := createPreDeployJobChart(
  260. ctx,
  261. appName,
  262. preDeployJobValues,
  263. c.Config().ServerConf.DefaultApplicationHelmRepoURL,
  264. registries,
  265. cluster,
  266. c.Repo(),
  267. )
  268. if err != nil {
  269. err = telemetry.Error(ctx, span, err, "error making config for pre-deploy job chart")
  270. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  271. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  272. return
  273. }
  274. _, err = helmAgent.InstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  275. if err != nil {
  276. err = telemetry.Error(ctx, span, err, "error installing pre-deploy job chart")
  277. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "install-pre-deploy-job-error", Value: err})
  278. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  279. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  280. _, uninstallChartErr := helmAgent.UninstallChart(ctx, fmt.Sprintf("%s-r", appName))
  281. if uninstallChartErr != nil {
  282. uninstallChartErr = telemetry.Error(ctx, span, err, "error uninstalling pre-deploy job chart after failed install")
  283. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(uninstallChartErr, http.StatusInternalServerError))
  284. }
  285. return
  286. }
  287. }
  288. conf := &helm.InstallChartConfig{
  289. Chart: chart,
  290. Name: appName,
  291. Namespace: namespace,
  292. Values: values,
  293. Cluster: cluster,
  294. Repo: c.Repo(),
  295. Registries: registries,
  296. }
  297. // create the app chart
  298. release, err := helmAgent.InstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  299. if err != nil {
  300. err = telemetry.Error(ctx, span, err, "error installing app chart")
  301. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  302. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  303. _, err = helmAgent.UninstallChart(ctx, appName)
  304. if err != nil {
  305. err = telemetry.Error(ctx, span, err, "error uninstalling app chart after failed install")
  306. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  307. }
  308. return
  309. }
  310. existing, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
  311. if err != nil {
  312. err = telemetry.Error(ctx, span, err, "error reading app from DB")
  313. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  314. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  315. return
  316. } else if existing.Name != "" {
  317. err = telemetry.Error(ctx, span, err, "app with name already exists in project")
  318. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  319. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
  320. return
  321. }
  322. app := &models.PorterApp{
  323. Name: appName,
  324. ClusterID: cluster.ID,
  325. ProjectID: project.ID,
  326. RepoName: request.RepoName,
  327. GitRepoID: request.GitRepoID,
  328. GitBranch: request.GitBranch,
  329. BuildContext: request.BuildContext,
  330. Builder: request.Builder,
  331. Buildpacks: request.Buildpacks,
  332. Dockerfile: request.Dockerfile,
  333. ImageRepoURI: request.ImageRepoURI,
  334. PullRequestURL: request.PullRequestURL,
  335. PorterYamlPath: request.PorterYamlPath,
  336. }
  337. // create the db entry
  338. porterApp, err := c.Repo().PorterApp().UpdatePorterApp(app)
  339. if err != nil {
  340. err = telemetry.Error(ctx, span, err, "error writing app to DB")
  341. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  342. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  343. return
  344. }
  345. if features.AreAgentDeployEventsEnabled(k8sAgent) {
  346. serviceDeploymentStatusMap := getServiceDeploymentMetadataFromValues(values, types.PorterAppEventStatus_Progressing)
  347. _, err = createNewPorterAppDeployEvent(ctx, serviceDeploymentStatusMap, porterApp.ID, 1, imageInfo.Tag, c.Repo().PorterAppEvent())
  348. } else {
  349. _, err = createOldPorterAppDeployEvent(ctx, types.PorterAppEventStatus_Success, porterApp.ID, 1, imageInfo.Tag, c.Repo().PorterAppEvent())
  350. }
  351. if err != nil {
  352. err = telemetry.Error(ctx, span, err, "error creating porter app event")
  353. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  354. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  355. return
  356. }
  357. c.WriteResult(w, r, porterApp.ToPorterAppTypeWithRevision(release.Version))
  358. } else {
  359. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "upgrading-application", Value: true})
  360. // create/update the pre-deploy job chart
  361. if request.OverrideRelease {
  362. if preDeployJobValues == nil {
  363. preDeployJobName := fmt.Sprintf("%s-r", appName)
  364. _, err := helmAgent.GetRelease(ctx, preDeployJobName, 0, false)
  365. if err == nil {
  366. // handle exception where the user has chosen to delete the release job
  367. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deleting-pre-deploy-job", Value: true})
  368. _, err = helmAgent.UninstallChart(ctx, preDeployJobName)
  369. if err != nil {
  370. err = telemetry.Error(ctx, span, err, "error uninstalling pre-deploy job chart")
  371. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  372. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  373. return
  374. }
  375. }
  376. } else {
  377. preDeployJobName := fmt.Sprintf("%s-r", appName)
  378. helmRelease, err := helmAgent.GetRelease(ctx, preDeployJobName, 0, false)
  379. if err != nil {
  380. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "creating-pre-deploy-job", Value: true})
  381. conf, err := createPreDeployJobChart(
  382. ctx,
  383. appName,
  384. preDeployJobValues,
  385. c.Config().ServerConf.DefaultApplicationHelmRepoURL,
  386. registries,
  387. cluster,
  388. c.Repo(),
  389. )
  390. if err != nil {
  391. err = telemetry.Error(ctx, span, err, "error making config for pre-deploy job chart")
  392. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  393. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  394. return
  395. }
  396. _, err = helmAgent.InstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  397. if err != nil {
  398. err = telemetry.Error(ctx, span, err, "error installing pre-deploy job chart")
  399. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "install-pre-deploy-job-error", Value: err})
  400. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  401. _, uninstallChartErr := helmAgent.UninstallChart(ctx, fmt.Sprintf("%s-r", appName))
  402. if uninstallChartErr != nil {
  403. uninstallChartErr = telemetry.Error(ctx, span, err, "error uninstalling pre-deploy job chart after failed install")
  404. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(uninstallChartErr, http.StatusInternalServerError))
  405. }
  406. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  407. return
  408. }
  409. } else {
  410. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "updating-pre-deploy-job", Value: true})
  411. chart, err := loader.LoadChartPublic(ctx, c.Config().Metadata.DefaultAppHelmRepoURL, "job", "")
  412. if err != nil {
  413. err = telemetry.Error(ctx, span, err, "error loading latest job chart")
  414. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  415. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  416. return
  417. }
  418. conf := &helm.UpgradeReleaseConfig{
  419. Name: helmRelease.Name,
  420. Cluster: cluster,
  421. Repo: c.Repo(),
  422. Registries: registries,
  423. Values: preDeployJobValues,
  424. Chart: chart,
  425. }
  426. _, err = helmAgent.UpgradeReleaseByValues(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection, false)
  427. if err != nil {
  428. err = telemetry.Error(ctx, span, err, "error upgrading pre-deploy job chart")
  429. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  430. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  431. return
  432. }
  433. }
  434. }
  435. }
  436. // update the app chart
  437. conf := &helm.InstallChartConfig{
  438. Chart: chart,
  439. Name: appName,
  440. Namespace: namespace,
  441. Values: values,
  442. Cluster: cluster,
  443. Repo: c.Repo(),
  444. Registries: registries,
  445. }
  446. // update the chart
  447. release, err := helmAgent.UpgradeInstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
  448. if err != nil {
  449. err = telemetry.Error(ctx, span, err, "error upgrading application")
  450. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  451. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  452. return
  453. }
  454. // update the DB entry
  455. app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
  456. if err != nil {
  457. err = telemetry.Error(ctx, span, err, "error reading app from DB")
  458. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  459. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  460. return
  461. }
  462. if app == nil {
  463. err = telemetry.Error(ctx, span, nil, "app with name does not exist in project")
  464. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  465. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
  466. return
  467. }
  468. if request.RepoName != "" {
  469. app.RepoName = request.RepoName
  470. }
  471. if request.GitBranch != "" {
  472. app.GitBranch = request.GitBranch
  473. }
  474. if request.BuildContext != "" {
  475. app.BuildContext = request.BuildContext
  476. }
  477. // handles deletion of builder,buildpacks, and dockerfile path
  478. if request.Builder != "" {
  479. if request.Builder == "null" {
  480. app.Builder = ""
  481. } else {
  482. app.Builder = request.Builder
  483. }
  484. }
  485. if request.Buildpacks != "" {
  486. if request.Buildpacks == "null" {
  487. app.Buildpacks = ""
  488. } else {
  489. app.Buildpacks = request.Buildpacks
  490. }
  491. }
  492. if request.Dockerfile != "" {
  493. if request.Dockerfile == "null" {
  494. app.Dockerfile = ""
  495. } else {
  496. app.Dockerfile = request.Dockerfile
  497. }
  498. }
  499. if request.ImageRepoURI != "" {
  500. app.ImageRepoURI = request.ImageRepoURI
  501. }
  502. if request.PullRequestURL != "" {
  503. app.PullRequestURL = request.PullRequestURL
  504. }
  505. telemetry.WithAttributes(
  506. span,
  507. telemetry.AttributeKV{Key: "updated-repo-name", Value: app.RepoName},
  508. telemetry.AttributeKV{Key: "updated-git-branch", Value: app.GitBranch},
  509. telemetry.AttributeKV{Key: "updated-build-context", Value: app.BuildContext},
  510. telemetry.AttributeKV{Key: "updated-builder", Value: app.Builder},
  511. telemetry.AttributeKV{Key: "updated-buildpacks", Value: app.Buildpacks},
  512. telemetry.AttributeKV{Key: "updated-dockerfile", Value: app.Dockerfile},
  513. telemetry.AttributeKV{Key: "updated-image-repo-uri", Value: app.ImageRepoURI},
  514. telemetry.AttributeKV{Key: "updated-pull-request-url", Value: app.PullRequestURL},
  515. )
  516. updatedPorterApp, err := c.Repo().PorterApp().UpdatePorterApp(app)
  517. if err != nil {
  518. err = telemetry.Error(ctx, span, err, "error writing updated app to DB")
  519. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  520. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  521. return
  522. }
  523. if features.AreAgentDeployEventsEnabled(k8sAgent) {
  524. serviceDeploymentStatusMap := getServiceDeploymentMetadataFromValues(values, types.PorterAppEventStatus_Progressing)
  525. _, err = createNewPorterAppDeployEvent(ctx, serviceDeploymentStatusMap, updatedPorterApp.ID, helmRelease.Version+1, imageInfo.Tag, c.Repo().PorterAppEvent())
  526. } else {
  527. _, err = createOldPorterAppDeployEvent(ctx, types.PorterAppEventStatus_Success, updatedPorterApp.ID, helmRelease.Version+1, imageInfo.Tag, c.Repo().PorterAppEvent())
  528. }
  529. if err != nil {
  530. err = telemetry.Error(ctx, span, err, "error creating porter app event")
  531. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-yaml-base64", Value: porterYamlBase64})
  532. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  533. return
  534. }
  535. c.WriteResult(w, r, updatedPorterApp.ToPorterAppTypeWithRevision(release.Version))
  536. }
  537. }
  538. type pollForRevisionNumberInput struct {
  539. ProjectID uint
  540. RevisionID string
  541. Namespace string
  542. K8sAgent *kubernetes.Agent
  543. }
  544. func pollForRevisionNumber(ctx context.Context, input pollForRevisionNumberInput) (int, error) {
  545. ctx, span := telemetry.NewSpan(ctx, "poll-for-revision-number")
  546. defer span.End()
  547. startTime := time.Now().UTC()
  548. for {
  549. if time.Now().UTC().After(startTime.Add(2 * time.Minute)) {
  550. return 0, telemetry.Error(ctx, span, nil, "timed out waiting for revision number")
  551. }
  552. deploymentList, err := input.K8sAgent.GetDeploymentsBySelector(ctx, input.Namespace, fmt.Sprintf("porter.run/app-revision-id=%s", input.RevisionID))
  553. if err != nil {
  554. return 0, telemetry.Error(ctx, span, err, "error getting app revision")
  555. }
  556. if deploymentList == nil {
  557. return 0, telemetry.Error(ctx, span, nil, "deployment list is nil")
  558. }
  559. if len(deploymentList.Items) > 0 {
  560. firstDeployment := deploymentList.Items[0]
  561. if len(firstDeployment.Spec.Template.Annotations) > 0 && firstDeployment.Spec.Template.Annotations["helm.sh/revision"] != "" {
  562. helmRevisionNumberString := firstDeployment.Spec.Template.Annotations["helm.sh/revision"]
  563. helmRevisionNumber, err := strconv.Atoi(helmRevisionNumberString)
  564. if err != nil {
  565. err = telemetry.Error(ctx, span, err, "error converting helm revision number to int")
  566. return 0, err
  567. }
  568. return helmRevisionNumber, nil
  569. }
  570. }
  571. time.Sleep(2 * time.Second)
  572. }
  573. }
  574. // createOldPorterAppDeployEvent creates an event for use in the activity feed
  575. // TODO: remove this method and all call-sites if this span no longer exists in telemetry for 4 consecutive weeks
  576. func createOldPorterAppDeployEvent(ctx context.Context, status types.PorterAppEventStatus, appID uint, revision int, tag string, repo repository.PorterAppEventRepository) (*models.PorterAppEvent, error) {
  577. ctx, span := telemetry.NewSpan(ctx, "create-old-porter-app-deploy-event")
  578. defer span.End()
  579. event := models.PorterAppEvent{
  580. ID: uuid.New(),
  581. Status: string(status),
  582. Type: "DEPLOY",
  583. TypeExternalSource: "KUBERNETES",
  584. PorterAppID: appID,
  585. Metadata: map[string]any{
  586. "revision": revision,
  587. "image_tag": tag,
  588. },
  589. }
  590. err := repo.CreateEvent(ctx, &event)
  591. if err != nil {
  592. return nil, err
  593. }
  594. if event.ID == uuid.Nil {
  595. return nil, err
  596. }
  597. return &event, nil
  598. }
  599. // createNewPorterAppDeployEvent creates an event for use in the activity feed, supplemented with information about the
  600. // deployed services in serviceStatusMap as well as the image tag being deployed
  601. func createNewPorterAppDeployEvent(
  602. ctx context.Context,
  603. serviceStatusMap map[string]types.ServiceDeploymentMetadata,
  604. appID uint,
  605. revision int,
  606. tag string,
  607. repo repository.PorterAppEventRepository,
  608. ) (*models.PorterAppEvent, error) {
  609. ctx, span := telemetry.NewSpan(ctx, "create-new-porter-app-deploy-event")
  610. defer span.End()
  611. // mark all pending deployments from the deploy event of the previous revision as canceled
  612. updatePreviousPorterAppDeployEvent(ctx, appID, revision, repo)
  613. deployEventStatus := types.PorterAppEventStatus_Success
  614. for _, metadata := range serviceStatusMap {
  615. if metadata.Status != types.PorterAppEventStatus_Success {
  616. deployEventStatus = types.PorterAppEventStatus_Progressing
  617. break
  618. }
  619. }
  620. event := models.PorterAppEvent{
  621. ID: uuid.New(),
  622. Status: string(deployEventStatus),
  623. Type: "DEPLOY",
  624. TypeExternalSource: "KUBERNETES",
  625. PorterAppID: appID,
  626. Metadata: map[string]any{
  627. "revision": revision,
  628. "image_tag": tag,
  629. "service_deployment_metadata": serviceStatusMap,
  630. },
  631. }
  632. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "revision", Value: revision}, telemetry.AttributeKV{Key: "image-tag", Value: tag})
  633. err := repo.CreateEvent(ctx, &event)
  634. if err != nil {
  635. err = telemetry.Error(ctx, span, err, "error creating porter app event")
  636. return nil, err
  637. }
  638. if event.ID == uuid.Nil {
  639. return nil, telemetry.Error(ctx, span, nil, "event id for newly created app event is nil")
  640. }
  641. return &event, nil
  642. }
  643. // updatePreviousPorterAppDeployEvent updates the previous deploy event to change the event status as well as all service statuses to CANCELED
  644. // if it is still in the PROGRESSING state. This is done to prevent the activity feed from showing an old deploy event as still in progress.
  645. func updatePreviousPorterAppDeployEvent(ctx context.Context, appID uint, revision int, repo repository.PorterAppEventRepository) {
  646. ctx, span := telemetry.NewSpan(ctx, "update-previous-porter-app-deploy-event")
  647. defer span.End()
  648. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "updating-previous-event", Value: false}, telemetry.AttributeKV{Key: "new-revision", Value: revision})
  649. if revision <= 1 {
  650. return
  651. }
  652. revisionFloat64 := float64(revision - 1)
  653. matchEvent, err := repo.ReadDeployEventByRevision(ctx, appID, revisionFloat64)
  654. if err != nil {
  655. _ = telemetry.Error(ctx, span, err, "error reading deploy event by revision")
  656. return
  657. }
  658. if matchEvent.ID == uuid.Nil {
  659. _ = telemetry.Error(ctx, span, nil, "could not find previous deploy event")
  660. return
  661. }
  662. if matchEvent.Status != string(types.PorterAppEventStatus_Progressing) {
  663. return
  664. }
  665. serviceStatus, ok := matchEvent.Metadata["service_deployment_metadata"]
  666. if !ok {
  667. _ = telemetry.Error(ctx, span, nil, "service deployment metadata not found in deploy event metadata")
  668. return
  669. }
  670. serviceDeploymentGenericMap, ok := serviceStatus.(map[string]interface{})
  671. if !ok {
  672. _ = telemetry.Error(ctx, span, nil, "service deployment metadata is not map[string]interface{}")
  673. return
  674. }
  675. serviceDeploymentMap := make(map[string]types.ServiceDeploymentMetadata)
  676. for k, v := range serviceDeploymentGenericMap {
  677. by, err := json.Marshal(v)
  678. if err != nil {
  679. _ = telemetry.Error(ctx, span, nil, "unable to marshal")
  680. return
  681. }
  682. var serviceDeploymentMetadata types.ServiceDeploymentMetadata
  683. err = json.Unmarshal(by, &serviceDeploymentMetadata)
  684. if err != nil {
  685. _ = telemetry.Error(ctx, span, nil, "unable to unmarshal")
  686. return
  687. }
  688. serviceDeploymentMap[k] = serviceDeploymentMetadata
  689. }
  690. for key, serviceDeploymentMetadata := range serviceDeploymentMap {
  691. if serviceDeploymentMetadata.Status == types.PorterAppEventStatus_Progressing {
  692. serviceDeploymentMetadata.Status = types.PorterAppEventStatus_Canceled
  693. serviceDeploymentMap[key] = serviceDeploymentMetadata
  694. }
  695. }
  696. matchEvent.Metadata["service_deployment_metadata"] = serviceDeploymentMap
  697. matchEvent.Status = string(types.PorterAppEventStatus_Canceled)
  698. err = repo.UpdateEvent(ctx, &matchEvent)
  699. if err != nil {
  700. _ = telemetry.Error(ctx, span, err, "error updating deploy event")
  701. return
  702. }
  703. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "updating-previous-event", Value: true})
  704. }
  705. func createPreDeployJobChart(
  706. ctx context.Context,
  707. stackName string,
  708. values map[string]interface{},
  709. repoUrl string,
  710. registries []*models.Registry,
  711. cluster *models.Cluster,
  712. repo repository.Repository,
  713. ) (*helm.InstallChartConfig, error) {
  714. chart, err := loader.LoadChartPublic(ctx, repoUrl, "job", "")
  715. if err != nil {
  716. return nil, err
  717. }
  718. releaseName := utils.PredeployJobNameFromPorterAppName(stackName)
  719. namespace := utils.NamespaceFromPorterAppName(stackName)
  720. return &helm.InstallChartConfig{
  721. Chart: chart,
  722. Name: releaseName,
  723. Namespace: namespace,
  724. Values: values,
  725. Cluster: cluster,
  726. Repo: repo,
  727. Registries: registries,
  728. }, nil
  729. }
  730. func cloneEnvGroup(c *CreatePorterAppHandler, w http.ResponseWriter, r *http.Request, agent *kubernetes.Agent, envGroups []string, namespace string) {
  731. for _, envGroupName := range envGroups {
  732. cm, _, err := agent.GetLatestVersionedConfigMap(envGroupName, "porter-env-group")
  733. if err != nil {
  734. if errors.Is(err, kubernetes.IsNotFoundError) {
  735. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  736. fmt.Errorf("error cloning env group: envgroup %s in namespace %s not found", envGroupName, "porter-env-group"), http.StatusNotFound,
  737. "no config map found for envgroup",
  738. ))
  739. return
  740. }
  741. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  742. return
  743. }
  744. secret, _, err := agent.GetLatestVersionedSecret(envGroupName, "porter-env-group")
  745. if err != nil {
  746. if errors.Is(err, kubernetes.IsNotFoundError) {
  747. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  748. fmt.Errorf("error cloning env group: envgroup %s in namespace %s not found", envGroupName, "porter-env-group"), http.StatusNotFound,
  749. "no k8s secret found for envgroup",
  750. ))
  751. return
  752. }
  753. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  754. return
  755. }
  756. vars := make(map[string]string)
  757. secretVars := make(map[string]string)
  758. for key, val := range cm.Data {
  759. if !strings.Contains(val, "PORTERSECRET") {
  760. vars[key] = val
  761. }
  762. }
  763. for key, val := range secret.Data {
  764. secretVars[key] = string(val)
  765. }
  766. configMap, err := envgroup.CreateEnvGroup(agent, types.ConfigMapInput{
  767. Name: envGroupName,
  768. Namespace: namespace,
  769. Variables: vars,
  770. SecretVariables: secretVars,
  771. })
  772. if err != nil {
  773. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  774. return
  775. }
  776. _, err = envgroup.ToEnvGroup(configMap)
  777. if err != nil {
  778. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  779. return
  780. }
  781. }
  782. }