create_env_group.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. package namespace
  2. import (
  3. "fmt"
  4. "net/http"
  5. "strings"
  6. "sync"
  7. "sigs.k8s.io/yaml"
  8. "helm.sh/helm/v3/pkg/release"
  9. v1 "k8s.io/api/core/v1"
  10. "github.com/porter-dev/porter/api/server/authz"
  11. "github.com/porter-dev/porter/api/server/handlers"
  12. "github.com/porter-dev/porter/api/server/shared"
  13. "github.com/porter-dev/porter/api/server/shared/apierrors"
  14. "github.com/porter-dev/porter/api/server/shared/config"
  15. "github.com/porter-dev/porter/api/types"
  16. "github.com/porter-dev/porter/internal/helm"
  17. "github.com/porter-dev/porter/internal/kubernetes/envgroup"
  18. "github.com/porter-dev/porter/internal/models"
  19. )
  20. type CreateEnvGroupHandler struct {
  21. handlers.PorterHandlerReadWriter
  22. authz.KubernetesAgentGetter
  23. }
  24. func NewCreateEnvGroupHandler(
  25. config *config.Config,
  26. decoderValidator shared.RequestDecoderValidator,
  27. writer shared.ResultWriter,
  28. ) *CreateEnvGroupHandler {
  29. return &CreateEnvGroupHandler{
  30. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  31. KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
  32. }
  33. }
  34. func (c *CreateEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  35. request := &types.CreateEnvGroupRequest{}
  36. if ok := c.DecodeAndValidate(w, r, request); !ok {
  37. return
  38. }
  39. namespace := r.Context().Value(types.NamespaceScope).(string)
  40. cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
  41. agent, err := c.GetAgent(r, cluster, namespace)
  42. if err != nil {
  43. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  44. return
  45. }
  46. envGroup, err := envgroup.GetEnvGroup(agent, request.Name, namespace, 0)
  47. // if the environment group exists and has MetaVersion=1, throw an error
  48. if envGroup != nil && envGroup.MetaVersion == 1 {
  49. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  50. fmt.Errorf("env group with that name already exists"),
  51. http.StatusNotFound,
  52. ))
  53. return
  54. }
  55. helmAgent, err := c.GetHelmAgent(r, cluster, namespace)
  56. if err != nil {
  57. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  58. return
  59. }
  60. configMap, err := envgroup.CreateEnvGroup(agent, types.ConfigMapInput{
  61. Name: request.Name,
  62. Namespace: namespace,
  63. Variables: request.Variables,
  64. SecretVariables: request.SecretVariables,
  65. })
  66. if err != nil {
  67. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  68. return
  69. }
  70. envGroup, err = envgroup.ToEnvGroup(configMap)
  71. if err != nil {
  72. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  73. return
  74. }
  75. releases, err := envgroup.GetSyncedReleases(helmAgent, configMap)
  76. if err != nil {
  77. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  78. return
  79. }
  80. c.WriteResult(w, r, envGroup)
  81. // trigger rollout of new applications after writing the result
  82. errors := rolloutApplications(c.Config(), cluster, helmAgent, envGroup, configMap, releases)
  83. if len(errors) > 0 {
  84. errStrArr := make([]string, 0)
  85. for _, err := range errors {
  86. errStrArr = append(errStrArr, err.Error())
  87. }
  88. c.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(fmt.Errorf(strings.Join(errStrArr, ","))))
  89. return
  90. }
  91. }
  92. func rolloutApplications(
  93. config *config.Config,
  94. cluster *models.Cluster,
  95. helmAgent *helm.Agent,
  96. envGroup *types.EnvGroup,
  97. configMap *v1.ConfigMap,
  98. releases []*release.Release,
  99. ) []error {
  100. registries, err := config.Repo.Registry().ListRegistriesByProjectID(cluster.ProjectID)
  101. if err != nil {
  102. return []error{err}
  103. }
  104. // construct the synced env section that should be written
  105. newSection := &SyncedEnvSection{
  106. Name: envGroup.Name,
  107. Version: envGroup.Version,
  108. }
  109. newSectionKeys := make([]SyncedEnvSectionKey, 0)
  110. for key, val := range configMap.Data {
  111. newSectionKeys = append(newSectionKeys, SyncedEnvSectionKey{
  112. Name: key,
  113. Secret: strings.Contains(val, "PORTERSECRET"),
  114. })
  115. }
  116. newSection.Keys = newSectionKeys
  117. // asynchronously update releases with that image repo uri
  118. var wg sync.WaitGroup
  119. mu := &sync.Mutex{}
  120. errors := make([]error, 0)
  121. for i, rel := range releases {
  122. index := i
  123. release := rel
  124. wg.Add(1)
  125. go func() {
  126. defer wg.Done()
  127. // read release via agent
  128. newConfig, err := getNewConfig(release.Config, newSection)
  129. if err != nil {
  130. mu.Lock()
  131. errors = append(errors, err)
  132. mu.Unlock()
  133. return
  134. }
  135. // if this is a job chart, update the config and set correct paused param to true
  136. if release.Chart.Name() == "job" {
  137. newConfig["paused"] = true
  138. }
  139. conf := &helm.UpgradeReleaseConfig{
  140. Name: releases[index].Name,
  141. Cluster: cluster,
  142. Repo: config.Repo,
  143. Registries: registries,
  144. Values: newConfig,
  145. }
  146. _, err = helmAgent.UpgradeReleaseByValues(conf, config.DOConf)
  147. if err != nil {
  148. mu.Lock()
  149. errors = append(errors, err)
  150. mu.Unlock()
  151. return
  152. }
  153. }()
  154. }
  155. wg.Wait()
  156. return errors
  157. }
  158. type SyncedEnvSection struct {
  159. Name string `json:"name" yaml:"name"`
  160. Version uint `json:"version" yaml:"version"`
  161. Keys []SyncedEnvSectionKey `json:"keys" yaml:"keys"`
  162. }
  163. type SyncedEnvSectionKey struct {
  164. Name string `json:"name" yaml:"name"`
  165. Secret bool `json:"secret" yaml:"secret"`
  166. }
  167. func getNewConfig(curr map[string]interface{}, syncedEnvSection *SyncedEnvSection) (map[string]interface{}, error) {
  168. // look for container.env.synced
  169. envConf, err := getNestedMap(curr, "container", "env")
  170. if err != nil {
  171. return nil, err
  172. }
  173. syncedEnvInter, syncedEnvExists := envConf["synced"]
  174. if !syncedEnvExists {
  175. return curr, nil
  176. } else {
  177. syncedArr := make([]*SyncedEnvSection, 0)
  178. syncedArrInter, ok := syncedEnvInter.([]interface{})
  179. if !ok {
  180. return nil, fmt.Errorf("could not convert to synced env section: not an array")
  181. }
  182. for _, syncedArrInterObj := range syncedArrInter {
  183. syncedArrObj := &SyncedEnvSection{}
  184. syncedArrInterObjMap, ok := syncedArrInterObj.(map[string]interface{})
  185. if !ok {
  186. continue
  187. }
  188. if nameField, nameFieldExists := syncedArrInterObjMap["name"]; nameFieldExists {
  189. syncedArrObj.Name, ok = nameField.(string)
  190. if !ok {
  191. continue
  192. }
  193. }
  194. if versionField, versionFieldExists := syncedArrInterObjMap["version"]; versionFieldExists {
  195. versionFloat, ok := versionField.(float64)
  196. if !ok {
  197. continue
  198. }
  199. syncedArrObj.Version = uint(versionFloat)
  200. }
  201. if keyField, keyFieldExists := syncedArrInterObjMap["keys"]; keyFieldExists {
  202. keyFieldInterArr, ok := keyField.([]interface{})
  203. if !ok {
  204. continue
  205. }
  206. keyFieldMapArr := make([]map[string]interface{}, 0)
  207. for _, keyFieldInter := range keyFieldInterArr {
  208. mapConv, ok := keyFieldInter.(map[string]interface{})
  209. if !ok {
  210. continue
  211. }
  212. keyFieldMapArr = append(keyFieldMapArr, mapConv)
  213. }
  214. keyFieldRes := make([]SyncedEnvSectionKey, 0)
  215. for _, keyFieldMap := range keyFieldMapArr {
  216. toAdd := SyncedEnvSectionKey{}
  217. if nameField, nameFieldExists := keyFieldMap["name"]; nameFieldExists {
  218. toAdd.Name, ok = nameField.(string)
  219. if !ok {
  220. continue
  221. }
  222. }
  223. if secretField, secretFieldExists := keyFieldMap["secret"]; secretFieldExists {
  224. toAdd.Secret, ok = secretField.(bool)
  225. if !ok {
  226. continue
  227. }
  228. }
  229. keyFieldRes = append(keyFieldRes, toAdd)
  230. }
  231. syncedArrObj.Keys = keyFieldRes
  232. }
  233. syncedArr = append(syncedArr, syncedArrObj)
  234. }
  235. resArr := make([]SyncedEnvSection, 0)
  236. foundMatch := false
  237. for _, candidate := range syncedArr {
  238. if candidate.Name == syncedEnvSection.Name {
  239. resArr = append(resArr, *syncedEnvSection)
  240. foundMatch = true
  241. } else {
  242. resArr = append(resArr, *candidate)
  243. }
  244. }
  245. if !foundMatch {
  246. return curr, nil
  247. }
  248. envConf["synced"] = resArr
  249. }
  250. // to remove all types that Helm may not be able to work with, we marshal to and from
  251. // yaml for good measure. Otherwise we get silly error messages like:
  252. // Upgrade failed: template: web/templates/deployment.yaml:138:40: executing \"web/templates/deployment.yaml\"
  253. // at <$syncedEnv.keys>: can't evaluate field keys in type namespace.SyncedEnvSection
  254. currYAML, err := yaml.Marshal(curr)
  255. if err != nil {
  256. return nil, err
  257. }
  258. res := make(map[string]interface{})
  259. err = yaml.Unmarshal([]byte(currYAML), &res)
  260. if err != nil {
  261. return nil, err
  262. }
  263. return res, nil
  264. }
  265. func getNestedMap(obj map[string]interface{}, fields ...string) (map[string]interface{}, error) {
  266. var res map[string]interface{}
  267. curr := obj
  268. for _, field := range fields {
  269. objField, ok := curr[field]
  270. if !ok {
  271. return nil, fmt.Errorf("%s not found", field)
  272. }
  273. res, ok = objField.(map[string]interface{})
  274. if !ok {
  275. return nil, fmt.Errorf("%s is not a nested object", field)
  276. }
  277. curr = res
  278. }
  279. return res, nil
  280. }