github.com/hazelops/ize@v1.1.12-0.20230915191306-97d7c0e48f11/internal/manager/serverless/docker.go (about)

     1  package serverless
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"github.com/docker/distribution/reference"
     7  	"github.com/docker/docker/api/types"
     8  	"github.com/docker/docker/api/types/container"
     9  	"github.com/docker/docker/api/types/filters"
    10  	"github.com/docker/docker/api/types/mount"
    11  	"github.com/docker/docker/api/types/strslice"
    12  	"github.com/docker/docker/client"
    13  	"github.com/docker/docker/pkg/jsonmessage"
    14  	"github.com/hazelops/ize/pkg/terminal"
    15  	"os"
    16  	"time"
    17  )
    18  
    19  func (sls *Manager) deployWithDocker(s terminal.Step) error {
    20  	cli, err := client.NewClientWithOpts(client.FromEnv)
    21  	if err != nil {
    22  		return err
    23  	}
    24  
    25  	image := "node:" + sls.App.NodeVersion
    26  
    27  	s.Update("%s: checking for Docker image: %s", sls.App.Name, image)
    28  
    29  	imageRef, err := reference.ParseNormalizedNamed(image)
    30  	if err != nil {
    31  		return fmt.Errorf("error parsing Docker image: %s", err)
    32  	}
    33  
    34  	imageList, err := cli.ImageList(context.Background(), types.ImageListOptions{
    35  		Filters: filters.NewArgs(filters.KeyValuePair{
    36  			Key:   "reference",
    37  			Value: reference.FamiliarString(imageRef),
    38  		}),
    39  	})
    40  	if err != nil {
    41  		return err
    42  	}
    43  
    44  	if len(imageList) == 0 {
    45  		s.Update("%s: pulling image: %s", sls.App.Name, image)
    46  
    47  		resp, err := cli.ImagePull(context.Background(), reference.FamiliarString(imageRef), types.ImagePullOptions{})
    48  		if err != nil {
    49  			return err
    50  		}
    51  		defer resp.Close()
    52  
    53  		err = jsonmessage.DisplayJSONMessagesStream(resp, s.TermOutput(), os.Stdout.Fd(), true, nil)
    54  		if err != nil {
    55  			return fmt.Errorf("unable to stream pull logs to the terminal: %s", err)
    56  		}
    57  	}
    58  
    59  	s.Update("%s: downloading npm modules...", sls.App.Name)
    60  
    61  	err = sls.npm(cli, []string{"npm", "install", "--save-dev"}, s)
    62  	if err != nil {
    63  		return fmt.Errorf("can't deploy %s: %w", sls.App.Name, err)
    64  	}
    65  
    66  	s.Done()
    67  
    68  	if sls.App.CreateDomain {
    69  		s.Update("%s: creating domain...", sls.App.Name)
    70  		err = sls.serverless(cli, []string{
    71  			"create_domain",
    72  			"--verbose",
    73  			"--region", sls.App.AwsRegion,
    74  			"--profile", sls.App.AwsProfile,
    75  			"--stage", sls.Project.Env,
    76  		}, s)
    77  		if err != nil {
    78  			return err
    79  		}
    80  
    81  		s.Done()
    82  	}
    83  
    84  	s.Update("%s: deploying app...", sls.App.Name)
    85  
    86  	err = sls.serverless(cli, []string{
    87  		"deploy",
    88  		"--config", sls.App.File,
    89  		"--service", sls.App.Name,
    90  		"--verbose",
    91  		"--region", sls.App.AwsRegion,
    92  		"--profile", sls.App.AwsProfile,
    93  		"--stage", sls.Project.Env,
    94  	}, s)
    95  	if err != nil {
    96  		s.Abort()
    97  		time.Sleep(time.Second)
    98  		return err
    99  	}
   100  
   101  	return nil
   102  }
   103  
   104  func (sls *Manager) removeWithDocker(s terminal.Step) error {
   105  	cli, err := client.NewClientWithOpts(client.FromEnv)
   106  	if err != nil {
   107  		return err
   108  	}
   109  
   110  	image := "node:" + sls.App.NodeVersion
   111  
   112  	s.Update("%s: checking for Docker image: %s", sls.App.Name, image)
   113  
   114  	imageRef, err := reference.ParseNormalizedNamed(image)
   115  	if err != nil {
   116  		return fmt.Errorf("error parsing Docker image: %s", err)
   117  	}
   118  
   119  	imageList, err := cli.ImageList(context.Background(), types.ImageListOptions{
   120  		Filters: filters.NewArgs(filters.KeyValuePair{
   121  			Key:   "reference",
   122  			Value: reference.FamiliarString(imageRef),
   123  		}),
   124  	})
   125  	if err != nil {
   126  		return err
   127  	}
   128  
   129  	if len(imageList) == 0 {
   130  		s.Update("%s: pulling image: %s", sls.App.Name, image)
   131  
   132  		resp, err := cli.ImagePull(context.Background(), reference.FamiliarString(imageRef), types.ImagePullOptions{})
   133  		if err != nil {
   134  			return err
   135  		}
   136  		defer resp.Close()
   137  
   138  		err = jsonmessage.DisplayJSONMessagesStream(resp, s.TermOutput(), os.Stdout.Fd(), true, nil)
   139  		if err != nil {
   140  			return fmt.Errorf("unable to stream pull logs to the terminal: %s", err)
   141  		}
   142  	}
   143  
   144  	s.Done()
   145  	s.Update("%s: destroying app...", sls.App.Name)
   146  
   147  	err = sls.serverless(cli, []string{
   148  		"remove",
   149  		"--config", sls.App.File,
   150  		"--service", sls.App.Name,
   151  		"--verbose",
   152  		"--region", sls.App.AwsRegion,
   153  		"--stage", sls.Project.Env,
   154  		"--profile", sls.App.AwsProfile,
   155  	}, s)
   156  	if err != nil {
   157  		s.Abort()
   158  		return err
   159  	}
   160  
   161  	return nil
   162  }
   163  
   164  func (sls *Manager) serverless(cli *client.Client, cmd []string, step terminal.Step) error {
   165  	command := []string{"serverless"}
   166  	command = append(command, cmd...)
   167  
   168  	contConfig := &container.Config{
   169  		Image:        fmt.Sprintf("node:%v", sls.App.NodeVersion),
   170  		Entrypoint:   strslice.StrSlice{"/usr/local/bin/npx"},
   171  		WorkingDir:   "/app",
   172  		Env:          sls.App.Env,
   173  		Tty:          true,
   174  		Cmd:          command,
   175  		AttachStdin:  true,
   176  		AttachStdout: true,
   177  		AttachStderr: true,
   178  	}
   179  
   180  	contHostConfig := sls.getHostConfig(sls.Project.Home, sls.Project.RootDir)
   181  
   182  	cont, err := cli.ContainerCreate(
   183  		context.Background(),
   184  		contConfig,
   185  		contHostConfig,
   186  		nil,
   187  		nil,
   188  		"ize_sls",
   189  	)
   190  	if err != nil {
   191  		return fmt.Errorf("can't deploy app: %w", err)
   192  	}
   193  
   194  	if err := cli.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{}); err != nil {
   195  		return fmt.Errorf("can't deploy app: %w", err)
   196  	}
   197  
   198  	body, err := cli.ContainerAttach(context.Background(), cont.ID, types.ContainerAttachOptions{Stream: true, Stdout: true, Stderr: true, Stdin: true})
   199  	if err != nil {
   200  		return err
   201  	}
   202  
   203  	msgs := make(chan []byte)
   204  	msgsErr := make(chan error)
   205  
   206  	go func() {
   207  		for {
   208  			msg, er := body.Reader.ReadBytes('\n')
   209  			if er != nil {
   210  				msgsErr <- er
   211  				return
   212  			}
   213  			msgs <- msg
   214  		}
   215  	}()
   216  
   217  msgLoop:
   218  	for {
   219  		select {
   220  		case msg := <-msgs:
   221  			fmt.Fprintf(step.TermOutput(), "%s", msg)
   222  		case <-msgsErr:
   223  			break msgLoop
   224  		}
   225  	}
   226  
   227  	wait, errC := cli.ContainerWait(context.Background(), cont.ID, container.WaitConditionRemoved)
   228  
   229  	select {
   230  	case status := <-wait:
   231  		if status.StatusCode == 0 {
   232  			return nil
   233  		}
   234  		return fmt.Errorf("container exit status code %d", status.StatusCode)
   235  	case err := <-errC:
   236  		return fmt.Errorf("can't deploy app: %w", err)
   237  	}
   238  }
   239  
   240  func (sls *Manager) npm(cli *client.Client, cmd []string, s terminal.Step) error {
   241  	contConfig := &container.Config{
   242  		WorkingDir:   "/app",
   243  		Image:        fmt.Sprintf("node:%v", sls.App.NodeVersion),
   244  		Tty:          true,
   245  		Cmd:          cmd,
   246  		AttachStdin:  true,
   247  		AttachStdout: true,
   248  		AttachStderr: true,
   249  		OpenStdin:    true,
   250  	}
   251  
   252  	contHostConfig := sls.getHostConfig(sls.Project.Home, sls.Project.RootDir)
   253  
   254  	cont, err := cli.ContainerCreate(
   255  		context.Background(),
   256  		contConfig,
   257  		contHostConfig,
   258  		nil,
   259  		nil,
   260  		"sls",
   261  	)
   262  	if err != nil {
   263  		return fmt.Errorf("can't deploy app: %w", err)
   264  	}
   265  
   266  	if err := cli.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{}); err != nil {
   267  		return fmt.Errorf("can't deploy app: %w", err)
   268  	}
   269  
   270  	body, err := cli.ContainerAttach(context.Background(), cont.ID, types.ContainerAttachOptions{Stream: true, Stdout: true})
   271  	if err != nil {
   272  		return err
   273  	}
   274  
   275  	msgs := make(chan []byte)
   276  	msgsErr := make(chan error)
   277  
   278  	go func() {
   279  		for {
   280  			msg, er := body.Reader.ReadBytes('\n')
   281  			if er != nil {
   282  				msgsErr <- er
   283  				return
   284  			}
   285  			msgs <- msg
   286  		}
   287  	}()
   288  
   289  msgLoop:
   290  	for {
   291  		select {
   292  		case msg := <-msgs:
   293  			fmt.Fprintf(s.TermOutput(), "%s", msg)
   294  		case <-msgsErr:
   295  			break msgLoop
   296  		}
   297  	}
   298  
   299  	defer close(msgs)
   300  	defer close(msgsErr)
   301  	defer body.Close()
   302  
   303  	wait, errC := cli.ContainerWait(context.Background(), cont.ID, container.WaitConditionNotRunning)
   304  
   305  	select {
   306  	case status := <-wait:
   307  		if status.StatusCode == 0 {
   308  			return nil
   309  		}
   310  		return fmt.Errorf("container exit status code %d", status.StatusCode)
   311  	case err := <-errC:
   312  		return fmt.Errorf("can't deploy app: %w", err)
   313  	}
   314  }
   315  
   316  func (sls *Manager) getHostConfig(homeDir, rootDir string) *container.HostConfig {
   317  	return &container.HostConfig{
   318  		AutoRemove: true,
   319  		Mounts: []mount.Mount{
   320  			{
   321  				Type:     mount.TypeBind,
   322  				ReadOnly: true,
   323  				Source:   fmt.Sprintf("%v/.aws", homeDir),
   324  				Target:   "/root/.aws",
   325  			},
   326  			{
   327  				Type:   mount.TypeBind,
   328  				Source: fmt.Sprintf("%s/%s", rootDir, sls.App.Path),
   329  				Target: "/app",
   330  			},
   331  			{
   332  				Type:   mount.TypeBind,
   333  				Source: fmt.Sprintf("%s/%s/.serverless/", rootDir, sls.App.Path),
   334  				Target: "/root/.config",
   335  			},
   336  			{
   337  				Type:   mount.TypeBind,
   338  				Source: fmt.Sprintf("%s/.npm/", rootDir),
   339  				Target: "/root/.npm",
   340  			},
   341  			{
   342  				Type:   mount.TypeVolume,
   343  				Source: sls.App.SLSNodeModuleCacheMount,
   344  				Target: "/app/node_modules",
   345  			},
   346  		},
   347  	}
   348  }