hooks.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  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. switchboardTypes "github.com/porter-dev/switchboard/pkg/types"
  11. )
  12. type DeployStackHook struct {
  13. Client *api.Client
  14. StackName string
  15. ProjectID, ClusterID uint
  16. AppResourceGroup *switchboardTypes.ResourceGroup
  17. BuildImageDriverName string
  18. }
  19. type StackConfig struct {
  20. Values map[string]interface{}
  21. Dependencies []types.Dependency
  22. }
  23. func (t *DeployStackHook) PreApply() error {
  24. return nil
  25. }
  26. func (t *DeployStackHook) DataQueries() map[string]interface{} {
  27. res := map[string]interface{}{
  28. "image": fmt.Sprintf("{$.%s.image}", t.BuildImageDriverName),
  29. }
  30. return res
  31. }
  32. // deploy the stack
  33. func (t *DeployStackHook) PostApply(driverOutput map[string]interface{}) error {
  34. client := config.GetAPIClient()
  35. namespace := fmt.Sprintf("porter-stack-%s", t.StackName)
  36. _, err := client.GetRelease(
  37. context.Background(),
  38. t.ProjectID,
  39. t.ClusterID,
  40. namespace,
  41. t.StackName,
  42. )
  43. shouldCreate := err != nil
  44. if err != nil {
  45. color.New(color.FgYellow).Printf("Could not read release for stack %s (%s): attempting creation\n", t.StackName, err.Error())
  46. } else {
  47. color.New(color.FgGreen).Printf("Found release for stack %s: attempting update\n", t.StackName)
  48. }
  49. return t.applyStack(t.AppResourceGroup, client, shouldCreate, driverOutput)
  50. }
  51. func (t *DeployStackHook) applyStack(applications *switchboardTypes.ResourceGroup, client *api.Client, shouldCreate bool, driverOutput map[string]interface{}) error {
  52. if applications == nil {
  53. return fmt.Errorf("no applications found")
  54. }
  55. err := insertImageInfoIntoApps(applications, driverOutput)
  56. if err != nil {
  57. return fmt.Errorf("unable to insert image info into apps: %w", err)
  58. }
  59. values, err := buildStackValues(applications)
  60. if err != nil {
  61. return err
  62. }
  63. deps, err := buildStackDependencies(applications, client, t.ProjectID)
  64. if err != nil {
  65. return err
  66. }
  67. stackConf := StackConfig{
  68. Values: values,
  69. Dependencies: deps,
  70. }
  71. if shouldCreate {
  72. err := t.createStack(client, stackConf)
  73. if err != nil {
  74. return fmt.Errorf("error creating stack %s: %w", t.StackName, err)
  75. }
  76. } else {
  77. err := t.updateStack(client, stackConf)
  78. if err != nil {
  79. return fmt.Errorf("error updating stack %s: %w", t.StackName, err)
  80. }
  81. }
  82. return nil
  83. }
  84. func insertImageInfoIntoApps(applications *switchboardTypes.ResourceGroup, driverOutput map[string]interface{}) error {
  85. image, ok := driverOutput["image"].(string)
  86. if !ok || image == "" {
  87. return fmt.Errorf("unable to find image in driver output")
  88. }
  89. // split image into image-path:tag format
  90. imageSpl := strings.Split(image, ":")
  91. if len(imageSpl) != 2 {
  92. return fmt.Errorf("invalid image format: must be image-path:tag format")
  93. }
  94. for _, resource := range applications.Resources {
  95. if resource.Config == nil {
  96. resource.Config = make(map[string]interface{})
  97. }
  98. values, ok := resource.Config["Values"].(map[string]interface{})
  99. if !ok {
  100. values = make(map[string]interface{})
  101. resource.Config["Values"] = values
  102. }
  103. image, ok := values["image"].(map[string]interface{})
  104. if !ok {
  105. image = make(map[string]interface{})
  106. values["image"] = image
  107. }
  108. image["repository"] = imageSpl[0]
  109. image["tag"] = imageSpl[1]
  110. }
  111. return nil
  112. }
  113. func (t *DeployStackHook) createStack(client *api.Client, stackConf StackConfig) error {
  114. err := client.CreateStack(
  115. context.Background(),
  116. t.ProjectID,
  117. t.ClusterID,
  118. &types.CreateStackReleaseRequest{
  119. StackName: t.StackName,
  120. Values: convertMap(stackConf.Values).(map[string]interface{}),
  121. Dependencies: stackConf.Dependencies,
  122. },
  123. )
  124. if err != nil {
  125. return err
  126. }
  127. return nil
  128. }
  129. func (t *DeployStackHook) updateStack(client *api.Client, stackConf StackConfig) error {
  130. err := client.UpdateStack(
  131. context.Background(),
  132. t.ProjectID,
  133. t.ClusterID,
  134. t.StackName,
  135. &types.CreateStackReleaseRequest{
  136. StackName: t.StackName,
  137. Values: convertMap(stackConf.Values).(map[string]interface{}),
  138. Dependencies: stackConf.Dependencies,
  139. },
  140. )
  141. if err != nil {
  142. return err
  143. }
  144. return nil
  145. }
  146. // this is necessary to marshal the resulting object during the request
  147. func convertMap(m interface{}) interface{} {
  148. switch m := m.(type) {
  149. case map[string]interface{}:
  150. for k, v := range m {
  151. m[k] = convertMap(v)
  152. }
  153. case map[interface{}]interface{}:
  154. result := map[string]interface{}{}
  155. for k, v := range m {
  156. result[k.(string)] = convertMap(v)
  157. }
  158. return result
  159. case []interface{}:
  160. for i, v := range m {
  161. m[i] = convertMap(v)
  162. }
  163. }
  164. return m
  165. }
  166. func (t *DeployStackHook) OnConsolidatedErrors(map[string]error) {}
  167. func (t *DeployStackHook) OnError(error) {}