apply.go 12 KB

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