Przeglądaj źródła

add builder options to deploy, improve docs

Alexander Belanger 5 lat temu
rodzic
commit
a24a17095e
2 zmienionych plików z 366 dodań i 118 usunięć
  1. 238 50
      cli/cmd/deploy.go
  2. 128 68
      cli/cmd/deploy/deploy.go

+ 238 - 50
cli/cmd/deploy.go

@@ -15,6 +15,49 @@ import (
 var deployCmd = &cobra.Command{
 	Use:   "deploy",
 	Short: "Builds and deploys a specified application given by the --app flag.",
+	Long: fmt.Sprintf(`
+%s 
+
+Builds and deploys a specified application given by the --app flag. For example:
+
+  %s
+
+If the application has a remote Git repository source configured, this command uses the latest commit 
+from the remote repo and branch to deploy an application. It will use the latest commit as the image 
+tag. 
+
+To build from a local directory, you must specify the --local flag. The path can be configured via the 
+--path flag. You can also overwrite the tag using the --tag flag. For example, to build from the 
+local directory ~/path-to-dir with the tag "testing":
+
+  %s
+
+If your application is set up to use a Dockerfile by default, you can use a buildpack via the flag 
+"--method pack". Conversely, if your application is set up to use a buildpack by default, you can 
+use a Dockerfile by passing the flag "--method docker". You can specify the relative path to a Dockerfile 
+in your remote Git repository. For example, if a Dockerfile is found at ./docker/prod.Dockerfile, you can 
+specify it as follows:
+
+  %s
+
+If an application does not have a remote Git repository source, this command will attempt to use a 
+cloud-native buildpack builder and build from the current directory. If this is the desired behavior,
+you do not need to configure additional flags:
+
+  %s
+
+If you would like to build from a Dockerfile instead, use the flag --dockerfile and "--method docker"
+as documented above. For example:
+
+  %s
+`,
+		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter deploy\":"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter deploy --app example-app"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter deploy --app remote-git-app --local --path ~/path-to-dir --tag testing"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter deploy --app remote-git-app --method docker --dockerfile ./docker/prod.Dockerfile"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter deploy --app local-app"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter deploy --app local-app --method docker --dockerfile ~/porter-test/prod.Dockerfile"),
+	),
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, deployFull)
 
@@ -27,18 +70,22 @@ var deployCmd = &cobra.Command{
 var deployGetEnvCmd = &cobra.Command{
 	Use:   "get-env",
 	Short: "Gets environment variables for a deployment for a specified application given by the --app flag.",
-	Long: fmt.Sprintf(`Gets environment variables for a deployment for a specified application given by the --app flag.
-By default, env variables are printed via stdout for use in downstream commands:
+	Long: fmt.Sprintf(`
+%s 
+
+Gets environment variables for a deployment for a specified application given by the --app 
+flag. By default, env variables are printed via stdout for use in downstream commands:
 
   %s
 
-Output can also be written to a dotenv file via the --file flag, which should specify the destination
-path for a .env file. For example:
+Output can also be written to a file via the --file flag, which should specify the 
+destination path for a .env file. For example:
 
   %s
 `,
-		color.New(color.FgGreen).Sprintf("porter deploy get-env --app <app>"),
-		color.New(color.FgGreen).Sprintf("porter deploy get-env --app <app> --file .env"),
+		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter deploy get-env\":"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter deploy get-env --app example-app | xargs"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter deploy get-env --app example-app --file .env"),
 	),
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, deployGetEnv)
@@ -52,6 +99,37 @@ path for a .env file. For example:
 var deployBuildCmd = &cobra.Command{
 	Use:   "build",
 	Short: "Builds a new version of the application specified by the --app flag.",
+	Long: fmt.Sprintf(`
+%s 
+
+Builds a new version of the application specified by the --app flag. Depending on the 
+configured settings, this command may work automatically or will require a specified 
+--method flag. 
+
+If you have configured the Dockerfile path and/or a build context for this application, 
+this command will by default use those settings, so you just need to specify the --app 
+flag:
+
+  %s
+
+If you have not linked the build-time requirements for this application, the command will
+use a local build. By default, the cloud-native buildpacks builder will automatically be run 
+from the current directory. If you would like to change the build method, you can do so by 
+using the --method flag, for example:
+
+  %s
+
+When using "--method docker", you can specify the path to the Dockerfile using the 
+--dockerfile flag. This will also override the Dockerfile path that you may have linked
+for the application:
+
+  %s
+`,
+		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter deploy build\":"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter deploy build --app example-app"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter deploy build --app example-app --method docker"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter deploy build --app example-app --method docker --dockerfile ./prod.Dockerfile"),
+	),
 	Run: func(cmd *cobra.Command, args []string) {
 		err := checkLoginAndRun(args, deployBuild)
 
@@ -61,11 +139,71 @@ var deployBuildCmd = &cobra.Command{
 	},
 }
 
+var deployPushCmd = &cobra.Command{
+	Use:   "push",
+	Short: "Pushes a new image for an application specified by the --app flag.",
+	Long: fmt.Sprintf(`
+%s 
+
+Pushes a new image for an application specified by the --app flag. This command uses
+the image repository saved in the application config by default. For example, if an 
+application "nginx" was created from the image repo "gcr.io/snowflake-123456/nginx", 
+the following command would push the image "gcr.io/snowflake-123456/nginx:new-tag":
+
+  %s
+
+This command will not use your pre-saved authentication set up via "docker login," so if you
+are using an image registry that was created outside of Porter, make sure that you have 
+linked it via "porter connect".
+`,
+		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter deploy push\":"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter deploy push --app nginx --tag new-tag"),
+	),
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, deployPush)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+var deployCallWebhookCmd = &cobra.Command{
+	Use:   "call-webhook",
+	Short: "Calls the webhook for an application specified by the --app flag.",
+	Long: fmt.Sprintf(`
+%s 
+
+Calls the webhook for an application specified by the --app flag. This webhook will 
+trigger a new deployment for the application, with the new image set. For example:
+
+  %s
+
+This command will by default call the webhook with image tag "latest," but you can 
+specify a different tag with the --tag flag:
+
+  %s
+`,
+		color.New(color.FgBlue, color.Bold).Sprintf("Help for \"porter deploy call-webhook\":"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter deploy call-webhook --app example-app"),
+		color.New(color.FgGreen, color.Bold).Sprintf("porter deploy call-webhook --app example-app --tag custom-tag"),
+	),
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, deployCallWebhook)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
 var app string
 var getEnvFileDest string
 var local bool
 var localPath string
 var tag string
+var dockerfile string
+var method string
 
 func init() {
 	rootCmd.AddCommand(deployCmd)
@@ -77,6 +215,8 @@ func init() {
 		"Application in the Porter dashboard",
 	)
 
+	deployCmd.MarkPersistentFlagRequired("app")
+
 	deployCmd.PersistentFlags().StringVar(
 		&namespace,
 		"namespace",
@@ -104,7 +244,21 @@ func init() {
 		"tag",
 		"t",
 		"",
-		"If local build, the specified tag to use, if not \"latest\"",
+		"the specified tag to use, if not \"latest\"",
+	)
+
+	deployCmd.PersistentFlags().StringVar(
+		&dockerfile,
+		"dockerfile",
+		"",
+		"the path to the dockerfile",
+	)
+
+	deployCmd.PersistentFlags().StringVar(
+		&method,
+		"method",
+		"",
+		"the build method to use (\"docker\" or \"pack\")",
 	)
 
 	deployCmd.AddCommand(deployGetEnvCmd)
@@ -117,68 +271,42 @@ func init() {
 	)
 
 	deployCmd.AddCommand(deployBuildCmd)
+	deployCmd.AddCommand(deployPushCmd)
+	deployCmd.AddCommand(deployCallWebhookCmd)
 }
 
 func deployFull(resp *api.AuthCheckResponse, client *api.Client, args []string) error {
 	color.New(color.FgGreen).Println("Deploying app:", app)
 
-	// initialize the deploy agent
-	deployAgent, err := deploy.NewDeployAgent(client, app, &deploy.DeployOpts{
-		ProjectID:   config.Project,
-		ClusterID:   config.Cluster,
-		Namespace:   namespace,
-		Local:       local,
-		LocalPath:   localPath,
-		OverrideTag: tag,
-	})
+	deployAgent, err := deployGetAgent(client)
 
 	if err != nil {
 		return err
 	}
 
-	buildEnv, err := deployAgent.GetBuildEnv()
+	err = deployBuildWithAgent(deployAgent)
 
 	if err != nil {
 		return err
 	}
 
-	// set the environment variables in the process
-	err = deployAgent.SetBuildEnv(buildEnv)
+	err = deployPushWithAgent(deployAgent)
 
 	if err != nil {
 		return err
 	}
 
-	// build the deployment
-	color.New(color.FgGreen).Println("Building docker image for", app)
-
-	err = deployAgent.Build()
+	err = deployCallWebhookWithAgent(deployAgent)
 
 	if err != nil {
 		return err
 	}
 
-	// push the deployment
-	color.New(color.FgGreen).Println("Deploying new application for", app)
-
-	err = deployAgent.Deploy()
-
-	if err != nil {
-		return err
-	}
-
-	color.New(color.FgGreen).Println("Successfully deployed", app)
-
 	return nil
 }
 
 func deployGetEnv(resp *api.AuthCheckResponse, client *api.Client, args []string) error {
-	// initialize the deploy agent
-	deployAgent, err := deploy.NewDeployAgent(client, app, &deploy.DeployOpts{
-		ProjectID: config.Project,
-		ClusterID: config.Cluster,
-		Namespace: namespace,
-	})
+	deployAgent, err := deployGetAgent(client)
 
 	if err != nil {
 		return err
@@ -202,19 +330,60 @@ func deployGetEnv(resp *api.AuthCheckResponse, client *api.Client, args []string
 }
 
 func deployBuild(resp *api.AuthCheckResponse, client *api.Client, args []string) error {
-	color.New(color.FgGreen).Println("Building app:", app)
+	deployAgent, err := deployGetAgent(client)
 
-	// initialize the deploy agent
-	deployAgent, err := deploy.NewDeployAgent(client, app, &deploy.DeployOpts{
-		ProjectID: config.Project,
-		ClusterID: config.Cluster,
-		Namespace: namespace,
-	})
+	if err != nil {
+		return err
+	}
+
+	return deployBuildWithAgent(deployAgent)
+}
+
+func deployPush(resp *api.AuthCheckResponse, client *api.Client, args []string) error {
+	deployAgent, err := deployGetAgent(client)
+
+	if err != nil {
+		return err
+	}
+
+	return deployPushWithAgent(deployAgent)
+}
+
+func deployCallWebhook(resp *api.AuthCheckResponse, client *api.Client, args []string) error {
+	deployAgent, err := deployGetAgent(client)
 
 	if err != nil {
 		return err
 	}
 
+	return deployCallWebhookWithAgent(deployAgent)
+}
+
+// HELPER METHODS
+func deployGetAgent(client *api.Client) (*deploy.DeployAgent, error) {
+	var buildMethod deploy.DeployBuildType
+
+	if method != "" {
+		buildMethod = deploy.DeployBuildType(method)
+	}
+
+	// initialize the deploy agent
+	return deploy.NewDeployAgent(client, app, &deploy.DeployOpts{
+		ProjectID:       config.Project,
+		ClusterID:       config.Cluster,
+		Namespace:       namespace,
+		Local:           local,
+		LocalPath:       localPath,
+		LocalDockerfile: dockerfile,
+		OverrideTag:     tag,
+		Method:          buildMethod,
+	})
+}
+
+func deployBuildWithAgent(deployAgent *deploy.DeployAgent) error {
+	// build the deployment
+	color.New(color.FgGreen).Println("Building docker image for", app)
+
 	buildEnv, err := deployAgent.GetBuildEnv()
 
 	if err != nil {
@@ -228,8 +397,27 @@ func deployBuild(resp *api.AuthCheckResponse, client *api.Client, args []string)
 		return err
 	}
 
-	// build the deployment
-	color.New(color.FgGreen).Println("Building docker image for", app)
-
 	return deployAgent.Build()
 }
+
+func deployPushWithAgent(deployAgent *deploy.DeployAgent) error {
+	// push the deployment
+	color.New(color.FgGreen).Println("Pushing new image for", app)
+
+	return deployAgent.Push()
+}
+
+func deployCallWebhookWithAgent(deployAgent *deploy.DeployAgent) error {
+	// push the deployment
+	color.New(color.FgGreen).Println("Calling webhook for", app)
+
+	err := deployAgent.CallWebhook()
+
+	if err != nil {
+		return err
+	}
+
+	color.New(color.FgGreen).Println("Successfully re-deployed", app)
+
+	return nil
+}

+ 128 - 68
cli/cmd/deploy/deploy.go

@@ -16,44 +16,45 @@ import (
 	"k8s.io/client-go/util/homedir"
 )
 
-// deployBuildType is the option to use as a builder
-type deployBuildType string
+// DeployBuildType is the option to use as a builder
+type DeployBuildType string
 
 const (
 	// uses local Docker daemon to build and push images
-	deployBuildTypeDocker deployBuildType = "docker"
+	deployBuildTypeDocker DeployBuildType = "docker"
 
 	// uses cloud-native build pack to build and push images
-	deployBuildTypePack deployBuildType = "pack"
+	deployBuildTypePack DeployBuildType = "pack"
 )
 
 // DeployAgent handles the deployment and redeployment of an application on Porter
 type DeployAgent struct {
 	App string
 
-	buildType   deployBuildType
-	client      *api.Client
-	release     *api.GetReleaseResponse
-	agent       *docker.Agent
-	opts        *DeployOpts
-	tag         string
-	envPrefix   string
-	env         map[string]string
-	imageExists bool
+	client         *api.Client
+	release        *api.GetReleaseResponse
+	agent          *docker.Agent
+	opts           *DeployOpts
+	tag            string
+	envPrefix      string
+	env            map[string]string
+	imageExists    bool
+	imageRepo      string
+	dockerfilePath string
 }
 
 // DeployOpts are the options for creating a new DeployAgent
 type DeployOpts struct {
-	ProjectID   uint
-	ClusterID   uint
-	Namespace   string
-	Local       bool
-	LocalPath   string
-	OverrideTag string
+	ProjectID       uint
+	ClusterID       uint
+	Namespace       string
+	Local           bool
+	LocalPath       string
+	LocalDockerfile string
+	OverrideTag     string
+	Method          DeployBuildType
 }
 
-var ErrNoGitActionConfig error = fmt.Errorf("specified release does not have a git action config")
-
 // NewDeployAgent creates a new DeployAgent given a Porter API client, application
 // name, and DeployOpts.
 func NewDeployAgent(client *api.Client, app string, opts *DeployOpts) (*DeployAgent, error) {
@@ -71,11 +72,6 @@ func NewDeployAgent(client *api.Client, app string, opts *DeployOpts) (*DeployAg
 		return nil, err
 	}
 
-	// if the git action config is nil, return an error
-	if release.GitActionConfig == nil {
-		return nil, ErrNoGitActionConfig
-	}
-
 	deployAgent.release = release
 
 	// set an environment prefix to avoid collisions
@@ -92,10 +88,54 @@ func NewDeployAgent(client *api.Client, app string, opts *DeployOpts) (*DeployAg
 
 	deployAgent.agent = agent
 
-	if release.GitActionConfig.DockerfilePath != "" {
-		deployAgent.buildType = deployBuildTypeDocker
+	// if build method is not set, determine based on release config
+	if opts.Method == "" {
+		if release.GitActionConfig != nil {
+			// if the git action config exists, and dockerfile path is not empty, build type
+			// is docker
+			if release.GitActionConfig.DockerfilePath != "" {
+				deployAgent.opts.Method = deployBuildTypeDocker
+			}
+
+			// otherwise build type is pack
+			deployAgent.opts.Method = deployBuildTypePack
+		} else {
+			// if the git action config does not exist, we use pack by default
+			deployAgent.opts.Method = deployBuildTypePack
+		}
+	}
+
+	if deployAgent.opts.Method == deployBuildTypeDocker {
+		if release.GitActionConfig != nil {
+			deployAgent.dockerfilePath = release.GitActionConfig.DockerfilePath
+		}
+
+		if deployAgent.opts.LocalDockerfile != "" {
+			deployAgent.dockerfilePath = deployAgent.opts.LocalDockerfile
+		}
+
+		if deployAgent.opts.LocalDockerfile == "" {
+			deployAgent.dockerfilePath = "./Dockerfile"
+		}
+	}
+
+	// if the git action config is not set, we use local builds since pulling remote source
+	// will fail. we set the image based on the git action config or the image written in the
+	// helm values
+	if release.GitActionConfig == nil {
+		deployAgent.opts.Local = true
+
+		imageRepo, err := deployAgent.getReleaseImage()
+
+		if err != nil {
+			return nil, err
+		}
+
+		deployAgent.imageRepo = imageRepo
+
+		deployAgent.dockerfilePath = deployAgent.opts.LocalDockerfile
 	} else {
-		deployAgent.buildType = deployBuildTypePack
+		deployAgent.imageRepo = release.GitActionConfig.ImageRepoURI
 	}
 
 	deployAgent.tag = opts.OverrideTag
@@ -194,7 +234,7 @@ func (d *DeployAgent) Build() error {
 		d.imageExists = false
 	}
 
-	if d.buildType == deployBuildTypeDocker {
+	if d.opts.Method == deployBuildTypeDocker {
 		return d.BuildDocker(dst, d.tag)
 	}
 
@@ -203,7 +243,7 @@ func (d *DeployAgent) Build() error {
 
 func (d *DeployAgent) BuildDocker(dst, tag string) error {
 	opts := &docker.BuildOpts{
-		ImageRepo:    d.release.GitActionConfig.ImageRepoURI,
+		ImageRepo:    d.imageRepo,
 		Tag:          tag,
 		BuildContext: dst,
 		Env:          d.env,
@@ -211,7 +251,7 @@ func (d *DeployAgent) BuildDocker(dst, tag string) error {
 
 	return d.agent.BuildLocal(
 		opts,
-		d.release.GitActionConfig.DockerfilePath,
+		d.dockerfilePath,
 	)
 }
 
@@ -219,8 +259,8 @@ func (d *DeployAgent) BuildPack(dst, tag string) error {
 	// retag the image with "pack-cache" tag so that it doesn't re-pull from the registry
 	if d.imageExists {
 		err := d.agent.TagImage(
-			fmt.Sprintf("%s:%s", d.release.GitActionConfig.ImageRepoURI, tag),
-			fmt.Sprintf("%s:%s", d.release.GitActionConfig.ImageRepoURI, "pack-cache"),
+			fmt.Sprintf("%s:%s", d.imageRepo, tag),
+			fmt.Sprintf("%s:%s", d.imageRepo, "pack-cache"),
 		)
 
 		if err != nil {
@@ -232,7 +272,7 @@ func (d *DeployAgent) BuildPack(dst, tag string) error {
 	packAgent := &pack.Agent{}
 
 	opts := &docker.BuildOpts{
-		ImageRepo: d.release.GitActionConfig.ImageRepoURI,
+		ImageRepo: d.imageRepo,
 		// We tag the image with a stable param "pack-cache" so that pack can use the
 		// local image without attempting to re-pull from registry. We handle getting
 		// registry credentials and pushing/pulling the image.
@@ -249,19 +289,16 @@ func (d *DeployAgent) BuildPack(dst, tag string) error {
 	}
 
 	return d.agent.TagImage(
-		fmt.Sprintf("%s:%s", d.release.GitActionConfig.ImageRepoURI, "pack-cache"),
-		fmt.Sprintf("%s:%s", d.release.GitActionConfig.ImageRepoURI, tag),
+		fmt.Sprintf("%s:%s", d.imageRepo, "pack-cache"),
+		fmt.Sprintf("%s:%s", d.imageRepo, tag),
 	)
 }
 
-func (d *DeployAgent) Deploy() error {
-	// push the created image
-	err := d.agent.PushImage(fmt.Sprintf("%s:%s", d.release.GitActionConfig.ImageRepoURI, d.tag))
-
-	if err != nil {
-		return err
-	}
+func (d *DeployAgent) Push() error {
+	return d.agent.PushImage(fmt.Sprintf("%s:%s", d.imageRepo, d.tag))
+}
 
+func (d *DeployAgent) CallWebhook() error {
 	releaseExt, err := d.client.GetReleaseWebhook(
 		context.Background(),
 		d.opts.ProjectID,
@@ -311,35 +348,27 @@ func (d *DeployAgent) getEnvFromRelease() (map[string]string, error) {
 	return mapEnvConfig, nil
 }
 
-type NestedMapFieldNotFoundError struct {
-	Field string
-}
-
-func (e *NestedMapFieldNotFoundError) Error() string {
-	return fmt.Sprintf("could not find field %s in configuration", e.Field)
-}
-
-func getNestedMap(obj map[string]interface{}, fields ...string) (map[string]interface{}, error) {
-	var res map[string]interface{}
-	curr := obj
+func (d *DeployAgent) getReleaseImage() (string, error) {
+	// pull the currently deployed image to use cache, if possible
+	imageConfig, err := getNestedMap(d.release.Config, "image")
 
-	for _, field := range fields {
-		objField, ok := curr[field]
+	if err != nil {
+		return "", fmt.Errorf("could not get image config from release: %s", err.Error())
+	}
 
-		if !ok {
-			return nil, &NestedMapFieldNotFoundError{field}
-		}
+	repoInterface, ok := imageConfig["repository"]
 
-		res, ok = objField.(map[string]interface{})
+	if !ok {
+		return "", fmt.Errorf("repository field does not exist for image")
+	}
 
-		if !ok {
-			return nil, fmt.Errorf("%s is not a nested object", field)
-		}
+	repoStr, ok := repoInterface.(string)
 
-		curr = res
+	if !ok {
+		return "", fmt.Errorf("could not cast image.image field to string")
 	}
 
-	return res, nil
+	return repoStr, nil
 }
 
 func (d *DeployAgent) pullCurrentReleaseImage() error {
@@ -362,9 +391,9 @@ func (d *DeployAgent) pullCurrentReleaseImage() error {
 		return fmt.Errorf("could not cast image.tag field to string")
 	}
 
-	fmt.Printf("attempting to pull image: %s\n", fmt.Sprintf("%s:%s", d.release.GitActionConfig.ImageRepoURI, tagStr))
+	fmt.Printf("attempting to pull image: %s\n", fmt.Sprintf("%s:%s", d.imageRepo, tagStr))
 
-	return d.agent.PullImage(fmt.Sprintf("%s:%s", d.release.GitActionConfig.ImageRepoURI, tagStr))
+	return d.agent.PullImage(fmt.Sprintf("%s:%s", d.imageRepo, tagStr))
 }
 
 func (d *DeployAgent) downloadRepoToDir(downloadURL string) (string, error) {
@@ -405,3 +434,34 @@ func (d *DeployAgent) downloadRepoToDir(downloadURL string) (string, error) {
 
 	return res, nil
 }
+
+type NestedMapFieldNotFoundError struct {
+	Field string
+}
+
+func (e *NestedMapFieldNotFoundError) Error() string {
+	return fmt.Sprintf("could not find field %s in configuration", e.Field)
+}
+
+func getNestedMap(obj map[string]interface{}, fields ...string) (map[string]interface{}, error) {
+	var res map[string]interface{}
+	curr := obj
+
+	for _, field := range fields {
+		objField, ok := curr[field]
+
+		if !ok {
+			return nil, &NestedMapFieldNotFoundError{field}
+		}
+
+		res, ok = objField.(map[string]interface{})
+
+		if !ok {
+			return nil, fmt.Errorf("%s is not a nested object", field)
+		}
+
+		curr = res
+	}
+
+	return res, nil
+}