2
0

apply.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. package stack
  2. import (
  3. "context"
  4. "fmt"
  5. "strings"
  6. "github.com/fatih/color"
  7. api "github.com/porter-dev/porter/api/client"
  8. "github.com/porter-dev/porter/api/types"
  9. "github.com/porter-dev/porter/cli/cmd/config"
  10. "github.com/porter-dev/porter/internal/telemetry"
  11. switchboardTypes "github.com/porter-dev/switchboard/pkg/types"
  12. "go.opentelemetry.io/otel/trace"
  13. "gopkg.in/yaml.v2"
  14. )
  15. type StackConf struct {
  16. apiClient *api.Client
  17. rawBytes []byte
  18. parsed *PorterStackYAML
  19. stackName, namespace string
  20. projectID, clusterID uint
  21. }
  22. func CreateV1BuildResources(ctx context.Context, client *api.Client, raw []byte, stackName string, projectID uint, clusterID uint) (*switchboardTypes.ResourceGroup, string, error) {
  23. ctx, span := telemetry.NewSpan(ctx, "create-v1-build-resources")
  24. defer span.End()
  25. v1File := &switchboardTypes.ResourceGroup{
  26. Version: "v1",
  27. Resources: []*switchboardTypes.Resource{
  28. {
  29. Name: "get-env",
  30. Driver: "os-env",
  31. },
  32. },
  33. }
  34. var builder string
  35. stackConf, err := createStackConf(ctx, span, client, raw, stackName, projectID, clusterID)
  36. if err != nil {
  37. err = telemetry.Error(ctx, span, err, "error creating stack config")
  38. return nil, "", err
  39. }
  40. telemetry.WithAttributes(
  41. span,
  42. telemetry.AttributeKV{Key: "application-name", Value: stackConf.stackName},
  43. telemetry.AttributeKV{Key: "project-id", Value: stackConf.projectID},
  44. telemetry.AttributeKV{Key: "cluster-id", Value: stackConf.clusterID},
  45. )
  46. var bi, pi *switchboardTypes.Resource
  47. if stackConf.parsed.Build != nil {
  48. bi, pi, builder, err = createV1BuildResourcesFromPorterYaml(ctx, stackConf)
  49. if err != nil {
  50. color.New(color.FgRed).Printf("Could not build using values specified in porter.yaml (%s), attempting to load stack build settings instead \n", err.Error())
  51. bi, pi, builder, err = createV1BuildResourcesFromDB(ctx, client, stackConf)
  52. if err != nil {
  53. err = telemetry.Error(ctx, span, err, "error creating build resources")
  54. return nil, "", err
  55. }
  56. }
  57. } else {
  58. color.New(color.FgYellow).Printf("No build values specified in porter.yaml, attempting to load stack build settings instead \n")
  59. bi, pi, builder, err = createV1BuildResourcesFromDB(ctx, client, stackConf)
  60. if err != nil {
  61. err = telemetry.Error(ctx, span, err, "error creating build resources")
  62. return nil, "", err
  63. }
  64. }
  65. v1File.Resources = append(v1File.Resources, bi, pi)
  66. preDeploy, cmd, err := maybeCreatePreDeployResource(
  67. ctx,
  68. client,
  69. stackConf.parsed.Release,
  70. stackConf.stackName,
  71. bi.Name,
  72. pi.Name,
  73. stackConf.projectID,
  74. stackConf.clusterID,
  75. stackConf.parsed.Env,
  76. )
  77. if err != nil {
  78. err = telemetry.Error(ctx, span, err, "error creating pre-deploy resource")
  79. return nil, "", err
  80. }
  81. if preDeploy != nil {
  82. telemetry.WithAttributes(
  83. span,
  84. telemetry.AttributeKV{Key: "pre-deploy-resource-name", Value: preDeploy.Name},
  85. telemetry.AttributeKV{Key: "pre-deploy-resource-driver", Value: preDeploy.Driver},
  86. telemetry.AttributeKV{Key: "pre-deploy-resource-source", Value: preDeploy.Source},
  87. telemetry.AttributeKV{Key: "pre-deploy-resource-target", Value: preDeploy.Target},
  88. )
  89. color.New(color.FgYellow).Printf("Found pre-deploy command to run before deploying apps: %s \n", cmd)
  90. v1File.Resources = append(v1File.Resources, preDeploy)
  91. } else {
  92. color.New(color.FgYellow).Printf("No pre-deploy command found in porter.yaml or helm. \n")
  93. }
  94. return v1File, builder, nil
  95. }
  96. func createStackConf(ctx context.Context, span trace.Span, client *api.Client, raw []byte, stackName string, projectID uint, clusterID uint) (*StackConf, error) {
  97. var parsed *PorterStackYAML
  98. if raw == nil {
  99. parsed = createDefaultPorterYaml()
  100. } else {
  101. parsed = &PorterStackYAML{}
  102. err := yaml.Unmarshal(raw, parsed)
  103. if err != nil {
  104. err = telemetry.Error(ctx, span, err, "error parsing porter.yaml")
  105. errMsg := composePreviewMessage("error parsing porter.yaml", Error)
  106. return nil, fmt.Errorf("%s: %w", errMsg, err)
  107. }
  108. }
  109. err := config.ValidateCLIEnvironment()
  110. if err != nil {
  111. err = telemetry.Error(ctx, span, err, "porter CLI is not configured correctly")
  112. errMsg := composePreviewMessage("porter CLI is not configured correctly", Error)
  113. return nil, fmt.Errorf("%s: %w", errMsg, err)
  114. }
  115. releaseEnvVars := getEnvFromRelease(ctx, client, stackName, projectID, clusterID)
  116. if releaseEnvVars != nil {
  117. color.New(color.FgYellow).Printf("Reading build env from release\n")
  118. parsed.Env = mergeStringMaps(parsed.Env, releaseEnvVars)
  119. }
  120. return &StackConf{
  121. apiClient: client,
  122. rawBytes: raw,
  123. parsed: parsed,
  124. stackName: stackName,
  125. projectID: projectID,
  126. clusterID: clusterID,
  127. namespace: fmt.Sprintf("porter-stack-%s", stackName),
  128. }, nil
  129. }
  130. func createV1BuildResourcesFromPorterYaml(ctx context.Context, stackConf *StackConf) (*switchboardTypes.Resource, *switchboardTypes.Resource, string, error) {
  131. ctx, span := telemetry.NewSpan(ctx, "create-v1-build-resources-from-porter-yaml")
  132. bi, err := stackConf.parsed.Build.getV1BuildImage(stackConf.parsed.Env, stackConf.namespace)
  133. if err != nil {
  134. err = telemetry.Error(ctx, span, err, "error creating build resource")
  135. return nil, nil, "", err
  136. }
  137. telemetry.WithAttributes(
  138. span,
  139. telemetry.AttributeKV{Key: "build-resource-name", Value: bi.Name},
  140. telemetry.AttributeKV{Key: "build-resource-driver", Value: bi.Driver},
  141. telemetry.AttributeKV{Key: "build-resource-source", Value: bi.Source},
  142. telemetry.AttributeKV{Key: "build-resource-target", Value: bi.Target},
  143. )
  144. pi, err := stackConf.parsed.Build.getV1PushImage(stackConf.namespace)
  145. if err != nil {
  146. err = telemetry.Error(ctx, span, err, "error creating push resource")
  147. return nil, nil, "", err
  148. }
  149. telemetry.WithAttributes(
  150. span,
  151. telemetry.AttributeKV{Key: "push-resource-name", Value: bi.Name},
  152. telemetry.AttributeKV{Key: "push-resource-driver", Value: bi.Driver},
  153. telemetry.AttributeKV{Key: "push-resource-source", Value: bi.Source},
  154. telemetry.AttributeKV{Key: "push-resource-target", Value: bi.Target},
  155. )
  156. return bi, pi, stackConf.parsed.Build.GetBuilder(), nil
  157. }
  158. func createV1BuildResourcesFromDB(ctx context.Context, client *api.Client, stackConf *StackConf) (*switchboardTypes.Resource, *switchboardTypes.Resource, string, error) {
  159. ctx, span := telemetry.NewSpan(ctx, "create-v1-build-resources-from-db")
  160. res, err := client.GetPorterApp(context.Background(), stackConf.projectID, stackConf.clusterID, stackConf.stackName)
  161. if err != nil {
  162. err = telemetry.Error(ctx, span, err, "error reading build info from DB")
  163. return nil, nil, "", err
  164. }
  165. if res == nil {
  166. err = telemetry.Error(ctx, span, err, "stack not found")
  167. return nil, nil, "", err
  168. }
  169. build := convertToBuild(res)
  170. bi, err := build.getV1BuildImage(stackConf.parsed.Env, stackConf.namespace)
  171. if err != nil {
  172. err = telemetry.Error(ctx, span, err, "error creating build resource")
  173. return nil, nil, "", err
  174. }
  175. telemetry.WithAttributes(
  176. span,
  177. telemetry.AttributeKV{Key: "build-resource-name", Value: bi.Name},
  178. telemetry.AttributeKV{Key: "build-resource-driver", Value: bi.Driver},
  179. telemetry.AttributeKV{Key: "build-resource-source", Value: bi.Source},
  180. telemetry.AttributeKV{Key: "build-resource-target", Value: bi.Target},
  181. )
  182. pi, err := build.getV1PushImage(stackConf.namespace)
  183. if err != nil {
  184. err = telemetry.Error(ctx, span, err, "error creating push resource")
  185. return nil, nil, "", err
  186. }
  187. telemetry.WithAttributes(
  188. span,
  189. telemetry.AttributeKV{Key: "push-resource-name", Value: bi.Name},
  190. telemetry.AttributeKV{Key: "push-resource-driver", Value: bi.Driver},
  191. telemetry.AttributeKV{Key: "push-resource-source", Value: bi.Source},
  192. telemetry.AttributeKV{Key: "push-resource-target", Value: bi.Target},
  193. )
  194. return bi, pi, build.GetBuilder(), nil
  195. }
  196. func convertToBuild(porterApp *types.PorterApp) Build {
  197. var context *string
  198. if porterApp.BuildContext != "" {
  199. context = &porterApp.BuildContext
  200. }
  201. var method *string
  202. var m string
  203. if porterApp.RepoName == "" {
  204. m = "registry"
  205. method = &m
  206. } else if porterApp.Dockerfile == "" {
  207. m = "pack"
  208. method = &m
  209. } else {
  210. m = "docker"
  211. method = &m
  212. }
  213. var builder *string
  214. if porterApp.Builder != "" {
  215. builder = &porterApp.Builder
  216. }
  217. var buildpacks []*string
  218. if porterApp.Buildpacks != "" {
  219. bpSlice := strings.Split(porterApp.Buildpacks, ",")
  220. buildpacks = make([]*string, len(bpSlice))
  221. for i, bp := range bpSlice {
  222. temp := bp
  223. buildpacks[i] = &temp
  224. }
  225. }
  226. var dockerfile *string
  227. if porterApp.Dockerfile != "" {
  228. dockerfile = &porterApp.Dockerfile
  229. }
  230. var image *string
  231. if porterApp.ImageRepoURI != "" {
  232. image = &porterApp.ImageRepoURI
  233. }
  234. return Build{
  235. Context: context,
  236. Method: method,
  237. Builder: builder,
  238. Buildpacks: buildpacks,
  239. Dockerfile: dockerfile,
  240. Image: image,
  241. }
  242. }
  243. func createDefaultPorterYaml() *PorterStackYAML {
  244. return &PorterStackYAML{
  245. Apps: nil,
  246. }
  247. }
  248. func getEnvFromRelease(ctx context.Context, client *api.Client, stackName string, projectID uint, clusterID uint) map[string]string {
  249. ctx, span := telemetry.NewSpan(ctx, "get-env-from-release")
  250. defer span.End()
  251. var envVarsStringMap map[string]string
  252. namespace := fmt.Sprintf("porter-stack-%s", stackName)
  253. release, err := client.GetRelease(
  254. ctx,
  255. projectID,
  256. clusterID,
  257. namespace,
  258. stackName,
  259. )
  260. if err == nil && release != nil {
  261. for key, val := range release.Config {
  262. if key != "global" && isMapStringInterface(val) {
  263. appConfig := val.(map[string]interface{})
  264. if appConfig != nil {
  265. if container, ok := appConfig["container"]; ok {
  266. if containerMap, ok := container.(map[string]interface{}); ok {
  267. if env, ok := containerMap["env"]; ok {
  268. if envMap, ok := env.(map[string]interface{}); ok {
  269. if normal, ok := envMap["normal"]; ok {
  270. if normalMap, ok := normal.(map[string]interface{}); ok {
  271. convertedMap, err := toStringMap(normalMap)
  272. if err == nil && len(convertedMap) > 0 {
  273. envVarsStringMap = convertedMap
  274. break
  275. }
  276. }
  277. }
  278. }
  279. }
  280. }
  281. }
  282. }
  283. }
  284. }
  285. }
  286. return envVarsStringMap
  287. }
  288. func isMapStringInterface(val interface{}) bool {
  289. _, ok := val.(map[string]interface{})
  290. return ok
  291. }
  292. func toStringMap(m map[string]interface{}) (map[string]string, error) {
  293. result := make(map[string]string)
  294. for k, v := range m {
  295. strVal, ok := v.(string)
  296. if !ok {
  297. return nil, fmt.Errorf("value for key %q is not a string", k)
  298. }
  299. result[k] = strVal
  300. }
  301. return result, nil
  302. }
  303. func mergeStringMaps(base, override map[string]string) map[string]string {
  304. result := make(map[string]string)
  305. if base == nil && override == nil {
  306. return result
  307. }
  308. for k, v := range base {
  309. result[k] = v
  310. }
  311. for k, v := range override {
  312. result[k] = v
  313. }
  314. return result
  315. }