2
0

parser.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. package parser
  2. import (
  3. "fmt"
  4. "strings"
  5. "github.com/porter-dev/porter/api/types"
  6. "github.com/porter-dev/porter/internal/helm"
  7. "github.com/porter-dev/porter/internal/models"
  8. "github.com/porter-dev/porter/internal/templater"
  9. "github.com/porter-dev/porter/internal/templater/infra"
  10. "github.com/porter-dev/porter/internal/templater/utils"
  11. "helm.sh/helm/v3/pkg/chart"
  12. "helm.sh/helm/v3/pkg/release"
  13. "k8s.io/client-go/dynamic"
  14. "sigs.k8s.io/yaml"
  15. td "github.com/porter-dev/porter/internal/templater/dynamic"
  16. tm "github.com/porter-dev/porter/internal/templater/helm/manifests"
  17. tv "github.com/porter-dev/porter/internal/templater/helm/values"
  18. )
  19. // ClientConfigDefault is a set of default clients to be used if a context in
  20. // form.yaml does not declare otherwise.
  21. type ClientConfigDefault struct {
  22. DynamicClient dynamic.Interface
  23. HelmAgent *helm.Agent
  24. HelmRelease *release.Release
  25. HelmChart *chart.Chart
  26. InfraOperation *models.Operation
  27. }
  28. // ContextConfig can read/write from a specified context (data source)
  29. type ContextConfig struct {
  30. FromType string // "live" or "declared"
  31. Capabilities []string // "read", "write"
  32. TemplateReader templater.TemplateReader
  33. TemplateWriter templater.TemplateWriter
  34. }
  35. // GetFormFromRelease returns the form by parsing a release's files. Returns nil if
  36. // the form is not found, throws an error if the form was found but there was a parsing
  37. // error.
  38. func GetFormFromRelease(def *ClientConfigDefault, rel *release.Release) (*types.FormYAML, error) {
  39. for _, file := range rel.Chart.Files {
  40. if strings.Contains(file.Name, "form.yaml") {
  41. return FormYAMLFromBytes(def, file.Data, "", "")
  42. }
  43. }
  44. return nil, nil
  45. }
  46. // FormYAMLFromBytes generates a usable form yaml from raw form config and a
  47. // set of default clients.
  48. //
  49. // stateType refers to the types of state that should be read. The two state types
  50. // are "live" and "declared" -- if stateType is "", this will read both live and
  51. // declared states.
  52. func FormYAMLFromBytes(def *ClientConfigDefault, bytes []byte, stateType, contextType string) (*types.FormYAML, error) {
  53. form, err := unqueriedFormYAMLFromBytes(bytes, contextType)
  54. if err != nil {
  55. return nil, err
  56. }
  57. lookup := formToLookupTable(def, form, stateType)
  58. // merge data from lookup
  59. data := make(map[string]interface{})
  60. for _, lookupVal := range lookup {
  61. if lookupVal != nil {
  62. queryRes, err := lookupVal.TemplateReader.Read()
  63. if err != nil {
  64. continue
  65. }
  66. for queryResKey, queryResVal := range queryRes {
  67. data[queryResKey] = queryResVal
  68. }
  69. }
  70. }
  71. for i, tab := range form.Tabs {
  72. for j, section := range tab.Sections {
  73. for k, content := range section.Contents {
  74. key := fmt.Sprintf("tabs[%d].sections[%d].contents[%d]", i, j, k)
  75. if val, ok := data[key]; ok {
  76. content.Value = val
  77. }
  78. if content.Value == nil && content.Settings.Default != nil {
  79. content.Value = []interface{}{content.Settings.Default}
  80. }
  81. }
  82. }
  83. }
  84. // if the client config defaults are Helm-related, this is a cluster-scoped resource
  85. if def.HelmAgent != nil || def.HelmChart != nil || def.HelmRelease != nil {
  86. form.IsClusterScoped = true
  87. }
  88. return form, nil
  89. }
  90. func FormStreamer(
  91. def *ClientConfigDefault,
  92. bytes []byte,
  93. stateType string,
  94. targetContext *types.FormContext,
  95. on templater.OnDataStream,
  96. stopCh <-chan struct{},
  97. ) error {
  98. form, err := unqueriedFormYAMLFromBytes(bytes, targetContext.Type)
  99. if err != nil {
  100. return err
  101. }
  102. lookup := formToLookupTable(def, form, stateType)
  103. for lookupContext, lookupVal := range lookup {
  104. if lookupVal != nil && areContextsEqual(targetContext, lookupContext) {
  105. err := lookupVal.TemplateReader.ReadStream(on, stopCh)
  106. if err != nil {
  107. continue
  108. }
  109. }
  110. }
  111. return nil
  112. }
  113. func areContextsEqual(context1, context2 *types.FormContext) bool {
  114. if context1.Type != context2.Type {
  115. return false
  116. }
  117. subset12 := isSubset(context1.Config, context2.Config)
  118. subset21 := isSubset(context2.Config, context1.Config)
  119. return subset12 && subset21
  120. }
  121. func isSubset(map1, map2 map[string]string) bool {
  122. subset12 := true
  123. for key1, val1 := range map1 {
  124. if val2, exists := map2[key1]; !exists || val2 != val1 {
  125. subset12 = false
  126. break
  127. }
  128. }
  129. return subset12
  130. }
  131. // unqueriedFormYAMLFromBytes returns a FormYAML without values queries populated
  132. func unqueriedFormYAMLFromBytes(bytes []byte, contextType string) (*types.FormYAML, error) {
  133. // parse bytes into object
  134. form := &types.FormYAML{}
  135. err := yaml.Unmarshal(bytes, form)
  136. if err != nil {
  137. return nil, err
  138. }
  139. // populate all context fields, with default set to helm/values with no config
  140. parent := &types.FormContext{}
  141. if contextType == "" {
  142. parent.Type = "helm/values"
  143. } else {
  144. parent.Type = contextType
  145. }
  146. for _, tab := range form.Tabs {
  147. if tab.Context == nil {
  148. tab.Context = parent
  149. }
  150. for _, section := range tab.Sections {
  151. if section.Context == nil {
  152. section.Context = tab.Context
  153. }
  154. for _, content := range section.Contents {
  155. if content.Context == nil {
  156. content.Context = section.Context
  157. }
  158. }
  159. }
  160. }
  161. return form, nil
  162. }
  163. // create map[*FormContext]*ContextConfig
  164. // assumes all contexts populated
  165. func formToLookupTable(def *ClientConfigDefault, form *types.FormYAML, stateType string) map[*types.FormContext]*ContextConfig {
  166. lookup := make(map[*types.FormContext]*ContextConfig)
  167. for i, tab := range form.Tabs {
  168. for j, section := range tab.Sections {
  169. for k, content := range section.Contents {
  170. if content.Context == nil {
  171. continue
  172. }
  173. if _, ok := lookup[content.Context]; !ok {
  174. lookup[content.Context] = formContextToContextConfig(def, content.Context, stateType)
  175. }
  176. if lookup[content.Context] == nil {
  177. continue
  178. }
  179. if content.Value != nil && fmt.Sprintf("%v", content.Value) != "" {
  180. // TODO -- case on whether value is proper query string, if not resolve it to a
  181. // proper query string
  182. query, err := utils.NewQuery(
  183. fmt.Sprintf("tabs[%d].sections[%d].contents[%d]", i, j, k),
  184. fmt.Sprintf("%v", content.Value),
  185. content.Settings.Default,
  186. )
  187. if err != nil {
  188. continue
  189. }
  190. if stateType == "" || stateType == lookup[content.Context].FromType {
  191. lookup[content.Context].TemplateReader.RegisterQuery(query)
  192. }
  193. } else if content.Variable != "" {
  194. // if variable field set without value field set, make variable field into jsonpath
  195. // query
  196. query, err := utils.NewQuery(
  197. fmt.Sprintf("tabs[%d].sections[%d].contents[%d]", i, j, k),
  198. fmt.Sprintf(".%v", content.Variable),
  199. content.Settings.Default,
  200. )
  201. if err != nil {
  202. continue
  203. }
  204. if stateType == "" || stateType == lookup[content.Context].FromType {
  205. lookup[content.Context].TemplateReader.RegisterQuery(query)
  206. }
  207. }
  208. }
  209. }
  210. }
  211. return lookup
  212. }
  213. // TODO -- this needs to be able to construct new context configs based on
  214. // configuration for each context, but right now just uses the default config
  215. // for everything
  216. func formContextToContextConfig(def *ClientConfigDefault, context *types.FormContext, stateType string) *ContextConfig {
  217. res := &ContextConfig{}
  218. if context.Type == "helm/values" && (stateType == "" || stateType == "declared") {
  219. res.FromType = "declared"
  220. res.Capabilities = []string{"read", "write"}
  221. res.TemplateReader = &tv.TemplateReader{
  222. Release: def.HelmRelease,
  223. Chart: def.HelmChart,
  224. }
  225. relName := ""
  226. if def.HelmRelease != nil {
  227. relName = def.HelmRelease.Name
  228. }
  229. res.TemplateWriter = &tv.TemplateWriter{
  230. Agent: def.HelmAgent,
  231. Chart: def.HelmChart,
  232. ReleaseName: relName,
  233. }
  234. } else if context.Type == "helm/manifests" && (stateType == "" || stateType == "live") {
  235. res.FromType = "live"
  236. res.Capabilities = []string{"read"}
  237. res.TemplateReader = &tm.TemplateReader{
  238. Release: def.HelmRelease,
  239. }
  240. } else if context.Type == "cluster" && (stateType == "" || stateType == "live") {
  241. res.FromType = "live"
  242. res.Capabilities = []string{"read"}
  243. // identify object based on passed config
  244. obj := &td.Object{
  245. Group: context.Config["group"],
  246. Version: context.Config["version"],
  247. Resource: context.Config["resource"],
  248. Namespace: context.Config["namespace"],
  249. Name: context.Config["name"],
  250. }
  251. res.TemplateReader = td.NewDynamicTemplateReader(def.DynamicClient, obj)
  252. } else if context.Type == "infra" && def.InfraOperation != nil {
  253. res.FromType = "declared"
  254. res.TemplateReader = &infra.OperationReader{
  255. Operation: def.InfraOperation,
  256. }
  257. } else {
  258. return nil
  259. }
  260. return res
  261. }