apply.go 8.5 KB

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