parse.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
  1. package porter_app
  2. import (
  3. "fmt"
  4. "strconv"
  5. "strings"
  6. "github.com/porter-dev/porter/api/server/shared/config"
  7. "github.com/porter-dev/porter/api/types"
  8. "github.com/porter-dev/porter/internal/helm/loader"
  9. "github.com/porter-dev/porter/internal/integrations/powerdns"
  10. "github.com/porter-dev/porter/internal/kubernetes"
  11. "github.com/porter-dev/porter/internal/kubernetes/domain"
  12. "github.com/porter-dev/porter/internal/repository"
  13. "github.com/porter-dev/porter/internal/templater/utils"
  14. "github.com/stefanmcshane/helm/pkg/chart"
  15. "gopkg.in/yaml.v2"
  16. )
  17. type PorterStackYAML struct {
  18. Version *string `yaml:"version"`
  19. Build *Build `yaml:"build"`
  20. Env map[string]string `yaml:"env"`
  21. SyncedEnv []*SyncedEnvSection `yaml:"synced_env"`
  22. Apps map[string]*App `yaml:"apps"`
  23. Release *App `yaml:"release"`
  24. }
  25. type Build struct {
  26. Context *string `yaml:"context" validate:"dir"`
  27. Method *string `yaml:"method" validate:"required,oneof=pack docker registry"`
  28. Builder *string `yaml:"builder" validate:"required_if=Method pack"`
  29. Buildpacks []*string `yaml:"buildpacks"`
  30. Dockerfile *string `yaml:"dockerfile" validate:"required_if=Method docker"`
  31. Image *string `yaml:"image" validate:"required_if=Method registry"`
  32. }
  33. type App struct {
  34. Run *string `yaml:"run" validate:"required"`
  35. Config map[string]interface{} `yaml:"config"`
  36. Type *string `yaml:"type" validate:"oneof=web worker job"`
  37. }
  38. type SubdomainCreateOpts struct {
  39. k8sAgent *kubernetes.Agent
  40. dnsRepo repository.DNSRecordRepository
  41. powerDnsClient *powerdns.Client
  42. appRootDomain string
  43. stackName string
  44. }
  45. type SyncedEnvSection struct {
  46. Name string `json:"name" yaml:"name"`
  47. Version uint `json:"version" yaml:"version"`
  48. Keys []SyncedEnvSectionKey `json:"keys" yaml:"keys"`
  49. }
  50. type SyncedEnvSectionKey struct {
  51. Name string `json:"name" yaml:"name"`
  52. Secret bool `json:"secret" yaml:"secret"`
  53. }
  54. func parse(
  55. porterYaml []byte,
  56. imageInfo types.ImageInfo,
  57. config *config.Config,
  58. projectID uint,
  59. userUpdate bool,
  60. envGroups []string,
  61. namespace string,
  62. existingValues map[string]interface{},
  63. existingDependencies []*chart.Dependency,
  64. opts SubdomainCreateOpts,
  65. injectLauncher bool,
  66. shouldCreate bool,
  67. ) (*chart.Chart, map[string]interface{}, map[string]interface{}, error) {
  68. parsed := &PorterStackYAML{}
  69. err := yaml.Unmarshal(porterYaml, parsed)
  70. if err != nil {
  71. return nil, nil, nil, fmt.Errorf("%s: %w", "error parsing porter.yaml", err)
  72. }
  73. synced_env := make([]*SyncedEnvSection, 0)
  74. for i := range envGroups {
  75. cm, _, err := opts.k8sAgent.GetLatestVersionedConfigMap(envGroups[i], namespace)
  76. if err != nil {
  77. return nil, nil, nil, fmt.Errorf("%s: %w", "error building values from porter.yaml", err)
  78. }
  79. versionStr, ok := cm.ObjectMeta.Labels["version"]
  80. if !ok {
  81. return nil, nil, nil, fmt.Errorf("error extracting version from config map")
  82. }
  83. versionInt, err := strconv.Atoi(versionStr)
  84. if err != nil {
  85. return nil, nil, nil, fmt.Errorf("error converting version to int: %w", err)
  86. }
  87. version := uint(versionInt)
  88. newSection := &SyncedEnvSection{
  89. Name: envGroups[i],
  90. Version: version,
  91. }
  92. newSectionKeys := make([]SyncedEnvSectionKey, 0)
  93. for key, val := range cm.Data {
  94. newSectionKeys = append(newSectionKeys, SyncedEnvSectionKey{
  95. Name: key,
  96. Secret: strings.Contains(val, "PORTERSECRET"),
  97. })
  98. }
  99. newSection.Keys = newSectionKeys
  100. synced_env = append(synced_env, newSection)
  101. }
  102. parsed.SyncedEnv = synced_env
  103. // fmt.Println("This is the config map:" ,cm)
  104. values, err := buildUmbrellaChartValues(parsed, imageInfo, existingValues, opts, injectLauncher, shouldCreate, userUpdate)
  105. if err != nil {
  106. return nil, nil, nil, fmt.Errorf("%s: %w", "error building values from porter.yaml", err)
  107. }
  108. convertedValues := convertMap(values).(map[string]interface{})
  109. chart, err := buildUmbrellaChart(parsed, config, projectID, existingDependencies)
  110. if err != nil {
  111. return nil, nil, nil, fmt.Errorf("%s: %w", "error building chart from porter.yaml", err)
  112. }
  113. // return the parsed release values for the release job chart, if they exist
  114. var preDeployJobValues map[string]interface{}
  115. if parsed.Release != nil && parsed.Release.Run != nil {
  116. preDeployJobValues = buildPreDeployJobChartValues(parsed.Release, parsed.Env, parsed.SyncedEnv, imageInfo, injectLauncher, existingValues, strings.TrimSuffix(strings.TrimPrefix(namespace, "porter-stack-"), "")+"-r", userUpdate)
  117. }
  118. return chart, convertedValues, preDeployJobValues, nil
  119. }
  120. func buildUmbrellaChartValues(
  121. parsed *PorterStackYAML,
  122. imageInfo types.ImageInfo,
  123. existingValues map[string]interface{},
  124. opts SubdomainCreateOpts,
  125. injectLauncher bool,
  126. shouldCreate bool,
  127. userUpdate bool,
  128. ) (map[string]interface{}, error) {
  129. values := make(map[string]interface{})
  130. if parsed.Apps == nil {
  131. if existingValues == nil {
  132. return nil, fmt.Errorf("porter.yaml must contain at least one app, or release must exist and have values")
  133. }
  134. }
  135. for name, app := range parsed.Apps {
  136. appType := getType(name, app)
  137. defaultValues := getDefaultValues(app, parsed.Env, parsed.SyncedEnv, appType, existingValues, name, userUpdate)
  138. convertedConfig := convertMap(app.Config).(map[string]interface{})
  139. helm_values := utils.DeepCoalesceValues(defaultValues, convertedConfig)
  140. // required to identify the chart type because of https://github.com/helm/helm/issues/9214
  141. helmName := getHelmName(name, appType)
  142. if existingValues != nil {
  143. if existingValues[helmName] != nil {
  144. existingValuesMap := existingValues[helmName].(map[string]interface{})
  145. helm_values = utils.DeepCoalesceValues(existingValuesMap, helm_values)
  146. }
  147. }
  148. validateErr := validateHelmValues(helm_values, shouldCreate, appType)
  149. if validateErr != "" {
  150. return nil, fmt.Errorf("error validating service \"%s\": %s", name, validateErr)
  151. }
  152. err := createSubdomainIfRequired(helm_values, opts) // modifies helm_values to add subdomains if necessary
  153. if err != nil {
  154. return nil, err
  155. }
  156. // just in case this slips by
  157. if appType == "web" {
  158. if helm_values["ingress"] == nil {
  159. helm_values["ingress"] = map[string]interface{}{
  160. "enabled": false,
  161. }
  162. }
  163. }
  164. values[helmName] = helm_values
  165. }
  166. // add back in the existing services that were not overwritten
  167. for k, v := range existingValues {
  168. if values[k] == nil {
  169. values[k] = v
  170. }
  171. }
  172. // prepend launcher to all start commands if we need to
  173. for _, v := range values {
  174. if serviceValues, ok := v.(map[string]interface{}); ok {
  175. if serviceValues["container"] != nil {
  176. containerMap := serviceValues["container"].(map[string]interface{})
  177. if containerMap["command"] != nil {
  178. command := containerMap["command"].(string)
  179. if injectLauncher && !strings.HasPrefix(command, "launcher") && !strings.HasPrefix(command, "/cnb/lifecycle/launcher") {
  180. containerMap["command"] = fmt.Sprintf("/cnb/lifecycle/launcher %s", command)
  181. }
  182. }
  183. }
  184. }
  185. }
  186. if imageInfo.Repository != "" && imageInfo.Tag != "" {
  187. values["global"] = map[string]interface{}{
  188. "image": map[string]interface{}{
  189. "repository": imageInfo.Repository,
  190. "tag": imageInfo.Tag,
  191. },
  192. }
  193. }
  194. return values, nil
  195. }
  196. // we can add to this function up later or use an alternative
  197. func validateHelmValues(values map[string]interface{}, shouldCreate bool, appType string) string {
  198. // currently, we only validate port on initial app create, because this will break any updates to existing apps with lower port numbers
  199. if shouldCreate {
  200. // validate port for web services
  201. if appType == "web" {
  202. containerMap, err := getNestedMap(values, "container")
  203. if err != nil {
  204. return "error checking port: misformatted values"
  205. } else {
  206. portVal, portExists := containerMap["port"]
  207. if portExists {
  208. portStr, pOK := portVal.(string)
  209. if !pOK {
  210. return "error checking port: no port in container"
  211. }
  212. port, err := strconv.Atoi(portStr)
  213. if err != nil || port < 1024 || port > 65535 {
  214. return "port must be a number between 1024 and 65535"
  215. }
  216. } else {
  217. return "port must be specified for web services"
  218. }
  219. }
  220. }
  221. }
  222. return ""
  223. }
  224. func buildPreDeployJobChartValues(release *App, env map[string]string, synced_env []*SyncedEnvSection, imageInfo types.ImageInfo, injectLauncher bool, existingValues map[string]interface{}, name string, userUpdate bool) map[string]interface{} {
  225. defaultValues := getDefaultValues(release, env, synced_env, "job", existingValues, name+"-r", userUpdate)
  226. convertedConfig := convertMap(release.Config).(map[string]interface{})
  227. helm_values := utils.DeepCoalesceValues(defaultValues, convertedConfig)
  228. if imageInfo.Repository != "" && imageInfo.Tag != "" {
  229. helm_values["image"] = map[string]interface{}{
  230. "repository": imageInfo.Repository,
  231. "tag": imageInfo.Tag,
  232. }
  233. }
  234. // prepend launcher if we need to
  235. if helm_values["container"] != nil {
  236. containerMap := helm_values["container"].(map[string]interface{})
  237. if containerMap["command"] != nil {
  238. command := containerMap["command"].(string)
  239. if injectLauncher && !strings.HasPrefix(command, "launcher") && !strings.HasPrefix(command, "/cnb/lifecycle/launcher") {
  240. containerMap["command"] = fmt.Sprintf("/cnb/lifecycle/launcher %s", command)
  241. }
  242. }
  243. }
  244. return helm_values
  245. }
  246. // func populateSyncedEnvGroups(release *App, opts SubdomainCreateOpts) {
  247. // // TODO
  248. // cm, _, err := opts.k8sAgent.GetLatestVersionedConfigMap()
  249. // fmt.Println("This is the config map:" ,cm)
  250. // }
  251. func getType(name string, app *App) string {
  252. if app.Type != nil {
  253. return *app.Type
  254. }
  255. if strings.Contains(name, "web") {
  256. return "web"
  257. }
  258. return "worker"
  259. }
  260. func getDefaultValues(app *App, env map[string]string, synced_env []*SyncedEnvSection, appType string, existingValues map[string]interface{}, name string, userUpdate bool) map[string]interface{} {
  261. var defaultValues map[string]interface{}
  262. var runCommand string
  263. if app.Run != nil {
  264. runCommand = *app.Run
  265. }
  266. var syncedEnvs []map[string]interface{}
  267. envConf, err := getStacksNestedMap(existingValues, name+"-"+appType, "container", "env")
  268. if !userUpdate && err == nil {
  269. syncedEnvs = envConf
  270. } else {
  271. syncedEnvs = deconstructSyncedEnvs(synced_env, env)
  272. }
  273. defaultValues = map[string]interface{}{
  274. "container": map[string]interface{}{
  275. "command": runCommand,
  276. "env": map[string]interface{}{
  277. "normal": CopyEnv(env),
  278. "synced": syncedEnvs,
  279. },
  280. },
  281. }
  282. return defaultValues
  283. }
  284. func deconstructSyncedEnvs(synced_env []*SyncedEnvSection, env map[string]string) []map[string]interface{} {
  285. synced := make([]map[string]interface{}, 0)
  286. for _, group := range synced_env {
  287. keys := make([]map[string]interface{}, 0)
  288. for _, key := range group.Keys {
  289. if _, exists := env[key.Name]; !exists {
  290. // Only include keys not present in env
  291. keys = append(keys, map[string]interface{}{
  292. "name": key.Name,
  293. "secret": key.Secret,
  294. })
  295. }
  296. }
  297. syncedGroup := map[string]interface{}{
  298. "keys": keys,
  299. "name": group.Name,
  300. "version": group.Version,
  301. }
  302. synced = append(synced, syncedGroup)
  303. }
  304. return synced
  305. }
  306. func buildUmbrellaChart(parsed *PorterStackYAML, config *config.Config, projectID uint, existingDependencies []*chart.Dependency) (*chart.Chart, error) {
  307. deps := make([]*chart.Dependency, 0)
  308. for alias, app := range parsed.Apps {
  309. var appType string
  310. if existingDependencies != nil {
  311. for _, dep := range existingDependencies {
  312. // this condition checks that the dependency is of the form <alias>-web or <alias>-wkr or <alias>-job, meaning it already exists in the chart
  313. if strings.HasPrefix(dep.Alias, fmt.Sprintf("%s-", alias)) && (strings.HasSuffix(dep.Alias, "-web") || strings.HasSuffix(dep.Alias, "-wkr") || strings.HasSuffix(dep.Alias, "-job")) {
  314. appType = getChartTypeFromHelmName(dep.Alias)
  315. if appType == "" {
  316. return nil, fmt.Errorf("unable to determine type of existing dependency")
  317. }
  318. }
  319. }
  320. // this is a new app, so we need to get the type from the app name or type
  321. if appType == "" {
  322. appType = getType(alias, app)
  323. }
  324. } else {
  325. appType = getType(alias, app)
  326. }
  327. selectedRepo := config.ServerConf.DefaultApplicationHelmRepoURL
  328. selectedVersion, err := getLatestTemplateVersion(appType, config, projectID)
  329. if err != nil {
  330. return nil, err
  331. }
  332. helmName := getHelmName(alias, appType)
  333. deps = append(deps, &chart.Dependency{
  334. Name: appType,
  335. Alias: helmName,
  336. Version: selectedVersion,
  337. Repository: selectedRepo,
  338. })
  339. }
  340. // add in the existing dependencies that were not overwritten
  341. for _, dep := range existingDependencies {
  342. if !dependencyExists(deps, dep) {
  343. // have to repair the dependency name because of https://github.com/helm/helm/issues/9214
  344. if strings.HasSuffix(dep.Name, "-web") || strings.HasSuffix(dep.Name, "-wkr") || strings.HasSuffix(dep.Name, "-job") {
  345. dep.Name = getChartTypeFromHelmName(dep.Name)
  346. }
  347. deps = append(deps, dep)
  348. }
  349. }
  350. chart, err := createChartFromDependencies(deps)
  351. if err != nil {
  352. return nil, err
  353. }
  354. return chart, nil
  355. }
  356. func dependencyExists(deps []*chart.Dependency, dep *chart.Dependency) bool {
  357. for _, d := range deps {
  358. if d.Alias == dep.Alias {
  359. return true
  360. }
  361. }
  362. return false
  363. }
  364. func createChartFromDependencies(deps []*chart.Dependency) (*chart.Chart, error) {
  365. metadata := &chart.Metadata{
  366. Name: "umbrella",
  367. Description: "Web application that is exposed to external traffic.",
  368. Version: "0.96.0",
  369. APIVersion: "v2",
  370. Home: "https://getporter.dev/",
  371. Icon: "https://user-images.githubusercontent.com/65516095/111255214-07d3da80-85ed-11eb-99e2-fddcbdb99bdb.png",
  372. Keywords: []string{
  373. "porter",
  374. "application",
  375. "service",
  376. "umbrella",
  377. },
  378. Type: "application",
  379. Dependencies: deps,
  380. }
  381. // create a new chart object with the metadata
  382. c := &chart.Chart{
  383. Metadata: metadata,
  384. }
  385. return c, nil
  386. }
  387. func getLatestTemplateVersion(templateName string, config *config.Config, projectID uint) (string, error) {
  388. repoIndex, err := loader.LoadRepoIndexPublic(config.ServerConf.DefaultApplicationHelmRepoURL)
  389. if err != nil {
  390. return "", fmt.Errorf("%s: %w", "unable to load porter chart repo", err)
  391. }
  392. templates := loader.RepoIndexToPorterChartList(repoIndex, config.ServerConf.DefaultApplicationHelmRepoURL)
  393. if err != nil {
  394. return "", fmt.Errorf("%s: %w", "unable to load porter chart list", err)
  395. }
  396. var version string
  397. // find the matching template name
  398. for _, template := range templates {
  399. if templateName == template.Name {
  400. version = template.Versions[0]
  401. break
  402. }
  403. }
  404. if version == "" {
  405. return "", fmt.Errorf("matching template version not found")
  406. }
  407. return version, nil
  408. }
  409. func convertMap(m interface{}) interface{} {
  410. switch m := m.(type) {
  411. case map[string]interface{}:
  412. for k, v := range m {
  413. m[k] = convertMap(v)
  414. }
  415. case map[interface{}]interface{}:
  416. result := map[string]interface{}{}
  417. for k, v := range m {
  418. result[k.(string)] = convertMap(v)
  419. }
  420. return result
  421. case []interface{}:
  422. for i, v := range m {
  423. m[i] = convertMap(v)
  424. }
  425. }
  426. return m
  427. }
  428. func CopyEnv(env map[string]string) map[string]interface{} {
  429. envCopy := make(map[string]interface{})
  430. if env == nil {
  431. return envCopy
  432. }
  433. for k, v := range env {
  434. if k == "" || v == "" {
  435. continue
  436. }
  437. envCopy[k] = v
  438. }
  439. return envCopy
  440. }
  441. func createSubdomainIfRequired(
  442. mergedValues map[string]interface{},
  443. opts SubdomainCreateOpts,
  444. ) error {
  445. // look for ingress.enabled and no custom domains set
  446. ingressMap, err := getNestedMap(mergedValues, "ingress")
  447. if err == nil {
  448. enabledVal, enabledExists := ingressMap["enabled"]
  449. if enabledExists {
  450. enabled, eOK := enabledVal.(bool)
  451. if eOK && enabled {
  452. // if custom domain, we don't need to create a subdomain
  453. customDomVal, customDomExists := ingressMap["custom_domain"]
  454. if customDomExists {
  455. customDomain, cOK := customDomVal.(bool)
  456. if cOK && customDomain {
  457. return nil
  458. }
  459. }
  460. // subdomain already exists, no need to create one
  461. if porterHosts, ok := ingressMap["porter_hosts"].([]interface{}); ok && len(porterHosts) > 0 {
  462. return nil
  463. }
  464. // in the case of ingress enabled but no custom domain, create subdomain
  465. dnsRecord, err := createDNSRecord(opts)
  466. if err != nil {
  467. return fmt.Errorf("error creating subdomain: %s", err.Error())
  468. }
  469. subdomain := dnsRecord.ExternalURL
  470. if ingressVal, ok := mergedValues["ingress"]; !ok {
  471. mergedValues["ingress"] = map[string]interface{}{
  472. "porter_hosts": []string{
  473. subdomain,
  474. },
  475. }
  476. } else {
  477. ingressValMap := ingressVal.(map[string]interface{})
  478. ingressValMap["porter_hosts"] = []string{
  479. subdomain,
  480. }
  481. }
  482. }
  483. }
  484. }
  485. return nil
  486. }
  487. func createDNSRecord(opts SubdomainCreateOpts) (*types.DNSRecord, error) {
  488. if opts.powerDnsClient == nil {
  489. return nil, fmt.Errorf("cannot create subdomain because powerdns client is nil")
  490. }
  491. endpoint, found, err := domain.GetNGINXIngressServiceIP(opts.k8sAgent.Clientset)
  492. if err != nil {
  493. return nil, err
  494. }
  495. if !found {
  496. return nil, fmt.Errorf("target cluster does not have nginx ingress")
  497. }
  498. createDomain := domain.CreateDNSRecordConfig{
  499. ReleaseName: opts.stackName,
  500. RootDomain: opts.appRootDomain,
  501. Endpoint: endpoint,
  502. }
  503. record := createDomain.NewDNSRecordForEndpoint()
  504. record, err = opts.dnsRepo.CreateDNSRecord(record)
  505. if err != nil {
  506. return nil, err
  507. }
  508. _record := domain.DNSRecord(*record)
  509. err = _record.CreateDomain(opts.powerDnsClient)
  510. if err != nil {
  511. return nil, err
  512. }
  513. return record.ToDNSRecordType(), nil
  514. }
  515. func getNestedMap(obj map[string]interface{}, fields ...string) (map[string]interface{}, error) {
  516. var res map[string]interface{}
  517. curr := obj
  518. for _, field := range fields {
  519. objField, ok := curr[field]
  520. if !ok {
  521. return nil, fmt.Errorf("%s not found", field)
  522. }
  523. res, ok = objField.(map[string]interface{})
  524. if !ok {
  525. return nil, fmt.Errorf("%s is not a nested object", field)
  526. }
  527. curr = res
  528. }
  529. return res, nil
  530. }
  531. func getHelmName(alias string, t string) string {
  532. var suffix string
  533. if t == "web" {
  534. suffix = "-web"
  535. } else if t == "worker" {
  536. suffix = "-wkr"
  537. } else if t == "job" {
  538. suffix = "-job"
  539. }
  540. return fmt.Sprintf("%s%s", alias, suffix)
  541. }
  542. func getChartTypeFromHelmName(name string) string {
  543. if strings.HasSuffix(name, "-web") {
  544. return "web"
  545. } else if strings.HasSuffix(name, "-wkr") {
  546. return "worker"
  547. } else if strings.HasSuffix(name, "-job") {
  548. return "job"
  549. }
  550. return ""
  551. }
  552. func attemptToGetImageInfoFromRelease(values map[string]interface{}) types.ImageInfo {
  553. imageInfo := types.ImageInfo{}
  554. if values == nil {
  555. return imageInfo
  556. }
  557. globalImage, err := getNestedMap(values, "global", "image")
  558. if err != nil {
  559. return imageInfo
  560. }
  561. repoVal, okRepo := globalImage["repository"]
  562. tagVal, okTag := globalImage["tag"]
  563. if okRepo && okTag {
  564. imageInfo.Repository = repoVal.(string)
  565. imageInfo.Tag = tagVal.(string)
  566. }
  567. return imageInfo
  568. }
  569. func getStacksNestedMap(obj map[string]interface{}, fields ...string) ([]map[string]interface{}, error) {
  570. var res map[string]interface{}
  571. curr := obj
  572. for _, field := range fields {
  573. objField, ok := curr[field]
  574. if !ok {
  575. return nil, fmt.Errorf("%s not found", field)
  576. }
  577. res, ok = objField.(map[string]interface{})
  578. if !ok {
  579. return nil, fmt.Errorf("%s is not a nested object", field)
  580. }
  581. curr = res
  582. }
  583. syncedInterface, ok := curr["synced"]
  584. if !ok {
  585. return nil, fmt.Errorf("synced not found")
  586. }
  587. synced, ok := syncedInterface.([]interface{})
  588. if !ok {
  589. return nil, fmt.Errorf("synced is not a slice of interface{}")
  590. }
  591. result := make([]map[string]interface{}, len(synced))
  592. for i, v := range synced {
  593. mapElement, ok := v.(map[string]interface{})
  594. if !ok {
  595. return nil, fmt.Errorf("element %d in synced is not a map[string]interface{}", i)
  596. }
  597. result[i] = mapElement
  598. }
  599. return result, nil
  600. }