2
0

apply.go 11 KB

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