apply.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. package stack
  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/models"
  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. if releaseEnvVars != nil {
  150. color.New(color.FgYellow).Printf("Reading build env from release\n")
  151. app.Env = mergeStringMaps(app.Env, releaseEnvVars)
  152. }
  153. ctx := context.Background()
  154. cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
  155. agent, err := c.GetAgent(r, cluster, "")
  156. if err != nil {
  157. errMsg := composePreviewMessage("porter CLI is not configured correctly", Error)
  158. return nil, fmt.Errorf("%s: %w", errMsg, err)
  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 getEnvFromRelease(client *api.Client, stackName string, projectID uint, clusterID uint) map[string]string {
  228. var envVarsStringMap map[string]string
  229. namespace := fmt.Sprintf("porter-stack-%s", stackName)
  230. release, err := client.GetRelease(
  231. context.Background(),
  232. projectID,
  233. clusterID,
  234. namespace,
  235. stackName,
  236. )
  237. if err == nil && release != nil {
  238. for key, val := range release.Config {
  239. if key != "global" && isMapStringInterface(val) {
  240. appConfig := val.(map[string]interface{})
  241. if appConfig != nil {
  242. if container, ok := appConfig["container"]; ok {
  243. if containerMap, ok := container.(map[string]interface{}); ok {
  244. if env, ok := containerMap["env"]; ok {
  245. if envMap, ok := env.(map[string]interface{}); ok {
  246. if normal, ok := envMap["normal"]; ok {
  247. if normalMap, ok := normal.(map[string]interface{}); ok {
  248. convertedMap, err := toStringMap(normalMap)
  249. if err == nil && len(convertedMap) > 0 {
  250. envVarsStringMap = convertedMap
  251. break
  252. }
  253. }
  254. }
  255. }
  256. }
  257. }
  258. }
  259. }
  260. }
  261. }
  262. }
  263. return envVarsStringMap
  264. }
  265. func isMapStringInterface(val interface{}) bool {
  266. _, ok := val.(map[string]interface{})
  267. return ok
  268. }
  269. func toStringMap(m map[string]interface{}) (map[string]string, error) {
  270. result := make(map[string]string)
  271. for k, v := range m {
  272. strVal, ok := v.(string)
  273. if !ok {
  274. return nil, fmt.Errorf("value for key %q is not a string", k)
  275. }
  276. result[k] = strVal
  277. }
  278. return result, nil
  279. }
  280. func mergeStringMaps(base, override map[string]string) map[string]string {
  281. result := make(map[string]string)
  282. if base == nil && override == nil {
  283. return result
  284. }
  285. for k, v := range base {
  286. result[k] = v
  287. }
  288. for k, v := range override {
  289. result[k] = v
  290. }
  291. return result
  292. }