create.go 33 KB

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