apply.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. package porter_app
  2. import (
  3. "context"
  4. "fmt"
  5. "os"
  6. "strconv"
  7. "strings"
  8. "github.com/fatih/color"
  9. api "github.com/porter-dev/porter/api/client"
  10. "github.com/porter-dev/porter/api/types"
  11. "github.com/porter-dev/porter/cli/cmd/config"
  12. "github.com/porter-dev/porter/internal/telemetry"
  13. switchboardTypes "github.com/porter-dev/switchboard/pkg/types"
  14. switchboardWorker "github.com/porter-dev/switchboard/pkg/worker"
  15. "gopkg.in/yaml.v3"
  16. )
  17. type StackConf struct {
  18. apiClient api.Client
  19. parsed *Application
  20. stackName, namespace string
  21. projectID, clusterID uint
  22. }
  23. // CreateApplicationDeploy creates everything needed to deploy a porter app
  24. func CreateApplicationDeploy(ctx context.Context, client api.Client, worker *switchboardWorker.Worker, app *Application, applicationName string, cliConf config.CLIConfig) ([]*switchboardTypes.Resource, error) {
  25. err := cliConf.ValidateCLIEnvironment()
  26. if err != nil {
  27. errMsg := composePreviewMessage("porter CLI is not configured correctly", Error)
  28. return nil, fmt.Errorf("%s: %w", errMsg, err)
  29. }
  30. // we need to know the builder so that we can inject launcher to the start command later if heroku builder is used
  31. var builder string
  32. resources, builder, err := createV1BuildResources(ctx, client, app, applicationName, cliConf.Project, cliConf.Cluster)
  33. if err != nil {
  34. return nil, fmt.Errorf("error parsing porter.yaml for build resources: %w", err)
  35. }
  36. applicationBytes, err := yaml.Marshal(app)
  37. if err != nil {
  38. return nil, fmt.Errorf("malformed application definition: %w", err)
  39. }
  40. deployAppHook := &DeployAppHook{
  41. Client: client,
  42. CLIConfig: cliConf,
  43. ApplicationName: applicationName,
  44. ProjectID: cliConf.Project,
  45. ClusterID: cliConf.Cluster,
  46. BuildImageDriverName: GetBuildImageDriverName(applicationName),
  47. PorterYAML: applicationBytes,
  48. Builder: builder,
  49. }
  50. worker.RegisterHook("deploy-app", deployAppHook)
  51. return resources, nil
  52. }
  53. // Create app event to signfy start of build
  54. func createAppEvent(ctx context.Context, client api.Client, applicationName string, projectId, clusterId uint) (string, error) {
  55. var req *types.CreateOrUpdatePorterAppEventRequest
  56. if os.Getenv("GITHUB_RUN_ID") != "" {
  57. req = &types.CreateOrUpdatePorterAppEventRequest{
  58. Status: "PROGRESSING",
  59. Type: types.PorterAppEventType_Build,
  60. TypeExternalSource: "GITHUB",
  61. Metadata: map[string]any{
  62. "action_run_id": os.Getenv("GITHUB_RUN_ID"),
  63. "org": os.Getenv("GITHUB_REPOSITORY_OWNER"),
  64. },
  65. }
  66. repoNameSplit := strings.Split(os.Getenv("GITHUB_REPOSITORY"), "/")
  67. if len(repoNameSplit) != 2 {
  68. return "", fmt.Errorf("unable to parse GITHUB_REPOSITORY")
  69. }
  70. req.Metadata["repo"] = repoNameSplit[1]
  71. actionRunID := os.Getenv("GITHUB_RUN_ID")
  72. if actionRunID != "" {
  73. arid, err := strconv.Atoi(actionRunID)
  74. if err != nil {
  75. return "", fmt.Errorf("unable to parse GITHUB_RUN_ID as int: %w", err)
  76. }
  77. req.Metadata["action_run_id"] = arid
  78. }
  79. repoOwnerAccountID := os.Getenv("GITHUB_REPOSITORY_OWNER_ID")
  80. if repoOwnerAccountID != "" {
  81. arid, err := strconv.Atoi(repoOwnerAccountID)
  82. if err != nil {
  83. return "", fmt.Errorf("unable to parse GITHUB_REPOSITORY_OWNER_ID as int: %w", err)
  84. }
  85. req.Metadata["github_account_id"] = arid
  86. }
  87. } else {
  88. req = &types.CreateOrUpdatePorterAppEventRequest{
  89. Status: "PROGRESSING",
  90. Type: types.PorterAppEventType_Build,
  91. TypeExternalSource: "GITHUB",
  92. Metadata: map[string]any{},
  93. }
  94. }
  95. event, err := client.CreateOrUpdatePorterAppEvent(ctx, projectId, clusterId, applicationName, req)
  96. if err != nil {
  97. return "", fmt.Errorf("unable to create porter app build event: %w", err)
  98. }
  99. return event.ID, nil
  100. }
  101. func createV1BuildResources(ctx context.Context, client api.Client, app *Application, stackName string, projectID uint, clusterID uint) ([]*switchboardTypes.Resource, string, error) {
  102. var builder string
  103. resources := make([]*switchboardTypes.Resource, 0)
  104. stackConf, err := createStackConf(ctx, client, app, stackName, projectID, clusterID)
  105. if err != nil {
  106. return nil, "", err
  107. }
  108. var bi, pi *switchboardTypes.Resource
  109. // look up build settings from DB if none specified in porter.yaml
  110. if stackConf.parsed.Build == nil {
  111. color.New(color.FgYellow).Printf("No build values specified in porter.yaml, attempting to load stack build settings instead \n")
  112. res, err := client.GetPorterApp(ctx, stackConf.projectID, stackConf.clusterID, stackConf.stackName)
  113. if err != nil {
  114. return nil, "", fmt.Errorf("unable to read build info from DB: %w", err)
  115. }
  116. converted := convertToBuild(res)
  117. stackConf.parsed.Build = &converted
  118. }
  119. // only include build and push steps if an image is not already specified
  120. if stackConf.parsed.Build.Image == nil {
  121. bi, pi, builder, err = createV1BuildResourcesFromPorterYaml(stackConf)
  122. if err != nil {
  123. return nil, "", err
  124. }
  125. resources = append(resources, bi, pi)
  126. // also excluding use of pre-deploy with pre-built imges
  127. preDeploy, cmd, err := createPreDeployResource(
  128. ctx,
  129. client,
  130. stackConf.parsed.Release,
  131. stackConf.stackName,
  132. bi.Name,
  133. pi.Name,
  134. stackConf.projectID,
  135. stackConf.clusterID,
  136. stackConf.parsed.Env,
  137. )
  138. if err != nil {
  139. return nil, "", err
  140. }
  141. if preDeploy != nil {
  142. color.New(color.FgYellow).Printf("Found pre-deploy command to run before deploying apps: %s \n", cmd)
  143. resources = append(resources, preDeploy)
  144. } else {
  145. color.New(color.FgYellow).Printf("No pre-deploy command found in porter.yaml or helm. \n")
  146. }
  147. }
  148. return resources, builder, nil
  149. }
  150. //nolint:unparam
  151. func createStackConf(ctx context.Context, client api.Client, app *Application, stackName string, projectID uint, clusterID uint) (*StackConf, error) {
  152. releaseEnvVars := getEnvFromRelease(ctx, client, stackName, projectID, clusterID)
  153. releaseEnvGroupVars := getEnvGroupFromRelease(ctx, client, stackName, projectID, clusterID)
  154. // releaseEnvVars will override releaseEnvGroupVars
  155. totalEnv := mergeStringMaps(releaseEnvGroupVars, releaseEnvVars)
  156. if totalEnv != nil {
  157. color.New(color.FgYellow).Printf("Reading build env from release\n")
  158. app.Env = mergeStringMaps(app.Env, totalEnv)
  159. }
  160. return &StackConf{
  161. apiClient: client,
  162. parsed: app,
  163. stackName: stackName,
  164. projectID: projectID,
  165. clusterID: clusterID,
  166. namespace: fmt.Sprintf("porter-stack-%s", stackName),
  167. }, nil
  168. }
  169. func createV1BuildResourcesFromPorterYaml(stackConf *StackConf) (*switchboardTypes.Resource, *switchboardTypes.Resource, string, error) {
  170. bi, err := stackConf.parsed.Build.getV1BuildImage(stackConf.stackName, stackConf.parsed.Env, stackConf.namespace)
  171. if err != nil {
  172. return nil, nil, "", err
  173. }
  174. pi, err := stackConf.parsed.Build.getV1PushImage(stackConf.stackName, stackConf.namespace)
  175. if err != nil {
  176. return nil, nil, "", err
  177. }
  178. return bi, pi, stackConf.parsed.Build.GetBuilder(), nil
  179. }
  180. func convertToBuild(porterApp *types.PorterApp) Build {
  181. var context *string
  182. if porterApp.BuildContext != "" {
  183. context = &porterApp.BuildContext
  184. }
  185. var method *string
  186. var m string
  187. if porterApp.RepoName == "" {
  188. m = "registry"
  189. method = &m
  190. } else if porterApp.Dockerfile == "" {
  191. m = "pack"
  192. method = &m
  193. } else {
  194. m = "docker"
  195. method = &m
  196. }
  197. var builder *string
  198. if porterApp.Builder != "" {
  199. builder = &porterApp.Builder
  200. }
  201. var buildpacks []*string
  202. if porterApp.Buildpacks != "" {
  203. bpSlice := strings.Split(porterApp.Buildpacks, ",")
  204. buildpacks = make([]*string, len(bpSlice))
  205. for i, bp := range bpSlice {
  206. temp := bp
  207. buildpacks[i] = &temp
  208. }
  209. }
  210. var dockerfile *string
  211. if porterApp.Dockerfile != "" {
  212. dockerfile = &porterApp.Dockerfile
  213. }
  214. var image *string
  215. if porterApp.ImageRepoURI != "" {
  216. image = &porterApp.ImageRepoURI
  217. }
  218. return Build{
  219. Context: context,
  220. Method: method,
  221. Builder: builder,
  222. Buildpacks: buildpacks,
  223. Dockerfile: dockerfile,
  224. Image: image,
  225. }
  226. }
  227. func getEnvGroupFromRelease(ctx context.Context, client api.Client, stackName string, projectID uint, clusterID uint) map[string]string {
  228. var envGroups []string
  229. envVarsGroupStringMap := make(map[string]string)
  230. ctx, span := telemetry.NewSpan(ctx, "get-env-from-release")
  231. telemetry.WithAttributes(span,
  232. telemetry.AttributeKV{Key: "project-id", Value: projectID},
  233. telemetry.AttributeKV{Key: "stack-name", Value: stackName},
  234. )
  235. namespace := fmt.Sprintf("porter-stack-%s", stackName)
  236. release, err := client.GetRelease(
  237. ctx,
  238. projectID,
  239. clusterID,
  240. namespace,
  241. stackName,
  242. )
  243. if err != nil {
  244. telemetry.Error(ctx, span, err, "error getting env groups from release")
  245. span.End()
  246. return envVarsGroupStringMap
  247. }
  248. if err == nil && release != nil {
  249. for _, val := range release.Config {
  250. // Check if the value is a map
  251. if appConfig, ok := val.(map[string]interface{}); ok {
  252. if labels, ok := appConfig["labels"]; ok {
  253. if labelsMap, ok := labels.(map[string]interface{}); ok {
  254. if envGroup, ok := labelsMap["porter.run/linked-environment-group"]; ok {
  255. envGroups = append(envGroups, fmt.Sprintf("%v", envGroup))
  256. }
  257. }
  258. }
  259. }
  260. }
  261. }
  262. if envGroups == nil {
  263. return envVarsGroupStringMap
  264. }
  265. envGroupList, err := client.ListEnvGroups(
  266. ctx,
  267. projectID,
  268. clusterID)
  269. if err != nil {
  270. telemetry.Error(ctx, span, err, "error getting env groups during build")
  271. span.End()
  272. return envVarsGroupStringMap
  273. }
  274. if err == nil {
  275. for _, groupName := range envGroups {
  276. for _, envGroupItem := range envGroupList.EnvironmentGroups {
  277. if envGroupItem.Name == groupName {
  278. for k, v := range envGroupItem.Variables {
  279. envVarsGroupStringMap[k] = v
  280. }
  281. for k, v := range envGroupItem.SecretVariables {
  282. envVarsGroupStringMap[k] = v
  283. }
  284. }
  285. }
  286. }
  287. }
  288. return envVarsGroupStringMap
  289. }
  290. func getEnvFromRelease(ctx context.Context, client api.Client, stackName string, projectID uint, clusterID uint) map[string]string {
  291. var envVarsStringMap map[string]string
  292. namespace := fmt.Sprintf("porter-stack-%s", stackName)
  293. release, err := client.GetRelease(
  294. ctx,
  295. projectID,
  296. clusterID,
  297. namespace,
  298. stackName,
  299. )
  300. if err == nil && release != nil {
  301. for key, val := range release.Config {
  302. if key != "global" && isMapStringInterface(val) {
  303. appConfig := val.(map[string]interface{})
  304. if appConfig != nil {
  305. if container, ok := appConfig["container"]; ok {
  306. if containerMap, ok := container.(map[string]interface{}); ok {
  307. if env, ok := containerMap["env"]; ok {
  308. if envMap, ok := env.(map[string]interface{}); ok {
  309. if normal, ok := envMap["normal"]; ok {
  310. if normalMap, ok := normal.(map[string]interface{}); ok {
  311. convertedMap, err := toStringMap(normalMap)
  312. if err == nil && len(convertedMap) > 0 {
  313. envVarsStringMap = convertedMap
  314. break
  315. }
  316. }
  317. }
  318. }
  319. }
  320. }
  321. }
  322. }
  323. }
  324. }
  325. }
  326. return envVarsStringMap
  327. }
  328. func isMapStringInterface(val interface{}) bool {
  329. _, ok := val.(map[string]interface{})
  330. return ok
  331. }
  332. func toStringMap(m map[string]interface{}) (map[string]string, error) {
  333. result := make(map[string]string)
  334. for k, v := range m {
  335. strVal, ok := v.(string)
  336. if !ok {
  337. return nil, fmt.Errorf("value for key %q is not a string", k)
  338. }
  339. result[k] = strVal
  340. }
  341. return result, nil
  342. }
  343. func mergeStringMaps(base, override map[string]string) map[string]string {
  344. result := make(map[string]string)
  345. if base == nil && override == nil {
  346. return result
  347. }
  348. for k, v := range base {
  349. result[k] = v
  350. }
  351. for k, v := range override {
  352. result[k] = v
  353. }
  354. return result
  355. }