apply.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. package v2
  2. import (
  3. "context"
  4. "fmt"
  5. "os"
  6. "strings"
  7. "github.com/fatih/color"
  8. api "github.com/porter-dev/porter/api/client"
  9. apiTypes "github.com/porter-dev/porter/api/types"
  10. "github.com/porter-dev/porter/cli/cmd/config"
  11. parser "github.com/porter-dev/switchboard/v2/pkg/parser"
  12. types "github.com/porter-dev/switchboard/v2/pkg/types"
  13. validator "github.com/porter-dev/switchboard/v2/pkg/validator"
  14. "github.com/porter-dev/switchboard/v2/pkg/worker"
  15. )
  16. const (
  17. constantsEnvGroup = "preview-env-constants"
  18. defaultCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789~`!@#$%^&*()_+-={}[]"
  19. )
  20. type PreviewApplier struct {
  21. apiClient *api.Client
  22. rawBytes []byte
  23. namespace string
  24. parsed *types.ParsedPorterYAML
  25. variablesMap map[string]string
  26. osEnv map[string]string
  27. envGroups map[string]*apiTypes.EnvGroup
  28. }
  29. func NewApplier(client *api.Client, raw []byte, namespace string) (*PreviewApplier, error) {
  30. parsed, err := parser.ParseRawBytes(raw)
  31. if err != nil {
  32. return nil, err
  33. }
  34. err = validator.ValidatePorterYAML(parsed)
  35. if err != nil {
  36. return nil, err
  37. }
  38. return &PreviewApplier{
  39. apiClient: client,
  40. rawBytes: raw,
  41. namespace: namespace,
  42. parsed: parsed,
  43. }, nil
  44. }
  45. func (a *PreviewApplier) Apply() error {
  46. // check if the namespace exists in the current project-cluster pair
  47. //
  48. // this is a sanity check to ensure that the user does not see any internal
  49. // errors that are caused by the namespace not existing
  50. nsList, err := a.apiClient.GetK8sNamespaces(
  51. context.Background(),
  52. config.GetCLIConfig().Project,
  53. config.GetCLIConfig().Cluster,
  54. )
  55. if err != nil {
  56. return fmt.Errorf("error listing namespaces for project '%d', cluster '%d': %w",
  57. config.GetCLIConfig().Project, config.GetCLIConfig().Cluster, err)
  58. }
  59. namespaces := *nsList
  60. nsFound := false
  61. for _, ns := range namespaces {
  62. if ns.Name == a.namespace {
  63. nsFound = true
  64. break
  65. }
  66. }
  67. if !nsFound {
  68. return fmt.Errorf("namespace '%s' does not exist in project '%d', cluster '%d'",
  69. a.namespace, config.GetCLIConfig().Project, config.GetCLIConfig().Cluster)
  70. }
  71. color.New(color.FgBlue).Printf("[porter.yaml v2] Applying preview environments with the following attributes:\n"+
  72. "\tHost: %s\n\tProject ID: %d\n\tCluster ID: %d\n\tNamespace: %s\n",
  73. config.GetCLIConfig().Host,
  74. config.GetCLIConfig().Project,
  75. config.GetCLIConfig().Cluster,
  76. a.namespace,
  77. ) // FIXME: use a scoped logger
  78. // err := a.readOSEnv()
  79. // if err != nil {
  80. // return err
  81. // }
  82. // err = a.processVariables()
  83. // if err != nil {
  84. // return err
  85. // }
  86. // err = a.processEnvGroups()
  87. // if err != nil {
  88. // return err
  89. // }
  90. w := worker.NewWorker()
  91. w.RegisterDriver("default", &DefaultDriver{
  92. Vars: a.variablesMap,
  93. Env: a.osEnv,
  94. APIClient: a.apiClient,
  95. Namespace: a.namespace,
  96. })
  97. w.SetDefaultDriver("default")
  98. return w.Apply(a.parsed.PorterYAML)
  99. }
  100. func (a *PreviewApplier) readOSEnv() error {
  101. color.New(color.FgBlue).Println("[porter.yaml v2] Reading OS environment variables") // FIXME: use a scoped logger
  102. env := os.Environ()
  103. osEnv := make(map[string]string)
  104. for _, e := range env {
  105. k, v, _ := strings.Cut(e, "=")
  106. kCopy := k
  107. if k != "" && v != "" && strings.HasPrefix(k, "PORTER_APPLY_") {
  108. // we only read in env variables that start with PORTER_APPLY_
  109. for strings.HasPrefix(k, "PORTER_APPLY_") {
  110. k = strings.TrimPrefix(k, "PORTER_APPLY_")
  111. }
  112. if k == "" {
  113. color.New(color.FgYellow).Printf("[porter.yaml v2] Ignoring invalid OS environment variable '%s'\n", kCopy) // FIXME: use a scoped logger
  114. }
  115. osEnv[k] = v
  116. }
  117. }
  118. a.osEnv = osEnv
  119. return nil
  120. }
  121. func (a *PreviewApplier) processEnvGroups() error {
  122. color.New(color.FgBlue).Println("[porter.yaml v2] Processing env groups") // FIXME: use a scoped logger
  123. for _, eg := range a.parsed.PorterYAML.EnvGroups.GetValue() {
  124. envGroup, err := a.apiClient.GetEnvGroup(
  125. context.Background(),
  126. config.GetCLIConfig().Project,
  127. config.GetCLIConfig().Cluster,
  128. a.namespace,
  129. &apiTypes.GetEnvGroupRequest{
  130. Name: eg.Name.GetValue(),
  131. },
  132. )
  133. if err != nil && strings.Contains(err.Error(), "env group not found") {
  134. cloneFrom := strings.Split(eg.CloneFrom.GetValue(), "/")
  135. if len(cloneFrom) != 2 {
  136. // this should not happen
  137. return fmt.Errorf("internal error: please let the Porter team know about this and quote the following "+
  138. "error:\n-----\nERROR: invalid env group clone_from format: %s", eg.CloneFrom.GetValue())
  139. }
  140. // clone the env group
  141. envGroup, err := a.apiClient.CloneEnvGroup(
  142. context.Background(),
  143. config.GetCLIConfig().Project,
  144. config.GetCLIConfig().Cluster,
  145. cloneFrom[0],
  146. &apiTypes.CloneEnvGroupRequest{
  147. SourceName: cloneFrom[1],
  148. TargetNamespace: a.namespace,
  149. TargetName: eg.Name.GetValue(),
  150. },
  151. )
  152. if err != nil {
  153. return fmt.Errorf("error cloning env group '%s' from '%s': %w", eg.Name.GetValue(),
  154. eg.CloneFrom.GetValue(), err)
  155. }
  156. a.envGroups[eg.Name.GetValue()] = &apiTypes.EnvGroup{
  157. Name: envGroup.Name,
  158. Variables: envGroup.Variables,
  159. }
  160. } else if err != nil {
  161. return fmt.Errorf("error checking for env group '%s': %w", eg.Name.GetValue(), err)
  162. } else {
  163. a.envGroups[eg.Name.GetValue()] = &apiTypes.EnvGroup{
  164. Name: envGroup.Name,
  165. Variables: envGroup.Variables,
  166. }
  167. }
  168. }
  169. return nil
  170. }
  171. func (a *PreviewApplier) processVariables() error {
  172. color.New(color.FgBlue).Println("[porter.yaml v2] Processing variables") // FIXME: use a scoped logger
  173. constantsMap := make(map[string]string)
  174. variablesMap := make(map[string]string)
  175. for _, v := range a.parsed.PorterYAML.Variables.GetValue() {
  176. if v.Once.GetValue() {
  177. // a constant which should be stored in the env group on first run
  178. if exists, err := a.constantExistsInEnvGroup(v.Name.GetValue()); err == nil {
  179. if exists == nil {
  180. // this should not happen
  181. return fmt.Errorf("internal error: please let the Porter team know about this and quote the following " +
  182. "error:\n-----\nERROR: checking for constant existence in env group returned nil with no error")
  183. }
  184. val := *exists
  185. if !val {
  186. // create the constant in the env group
  187. if v.Value.GetValue() != "" {
  188. constantsMap[v.Name.GetValue()] = v.Value.GetValue()
  189. } else if v.Random.GetValue() {
  190. constantsMap[v.Name.GetValue()] = randomString(v.Length.GetValue(), defaultCharset)
  191. } else {
  192. // this should not happen
  193. return fmt.Errorf("internal error: please let the Porter team know about this and quote the following "+
  194. "error:\n-----\nERROR: for variable '%s', random is false and value is empty", v.Name.GetValue())
  195. }
  196. }
  197. } else {
  198. return fmt.Errorf("error checking for existence of constant %s: %w", v.Name.GetValue(), err)
  199. }
  200. } else {
  201. if v.Value.GetValue() != "" {
  202. variablesMap[v.Name.GetValue()] = v.Value.GetValue()
  203. } else if v.Random.GetValue() {
  204. variablesMap[v.Name.GetValue()] = randomString(v.Length.GetValue(), defaultCharset)
  205. } else {
  206. // this should not happen
  207. return fmt.Errorf("internal error: please let the Porter team know about this and quote the following "+
  208. "error:\n-----\nERROR: for variable '%s', random is false and value is empty", v.Name.GetValue())
  209. }
  210. }
  211. }
  212. if len(constantsMap) > 0 {
  213. // we need to create these constants in the env group
  214. _, err := a.apiClient.CreateEnvGroup(
  215. context.Background(),
  216. config.GetCLIConfig().Project,
  217. config.GetCLIConfig().Cluster,
  218. a.namespace,
  219. &apiTypes.CreateEnvGroupRequest{
  220. Name: constantsEnvGroup,
  221. Variables: constantsMap,
  222. },
  223. )
  224. if err != nil {
  225. return fmt.Errorf("error creating constants (variables with once set to true) in env group: %w", err)
  226. }
  227. for k, v := range constantsMap {
  228. variablesMap[k] = v
  229. }
  230. }
  231. a.variablesMap = variablesMap
  232. return nil
  233. }
  234. func (a *PreviewApplier) constantExistsInEnvGroup(name string) (*bool, error) {
  235. apiResponse, err := a.apiClient.GetEnvGroup(
  236. context.Background(),
  237. config.GetCLIConfig().Project,
  238. config.GetCLIConfig().Cluster,
  239. a.namespace,
  240. &apiTypes.GetEnvGroupRequest{
  241. Name: constantsEnvGroup,
  242. // we do not care about the version because it always needs to be the latest
  243. },
  244. )
  245. if err != nil {
  246. if strings.Contains(err.Error(), "env group not found") {
  247. return boolean(false), nil
  248. }
  249. return nil, err
  250. }
  251. if _, ok := apiResponse.Variables[name]; ok {
  252. return boolean(true), nil
  253. }
  254. return boolean(false), nil
  255. }