Skip to content

Commit

Permalink
feat(apps): Add deploy
Browse files Browse the repository at this point in the history
Co-authored-by: Ali Hamidi <[email protected]>
Co-authored-by: Brett Goulder <[email protected]>
  • Loading branch information
3 people committed Feb 19, 2022
1 parent 9b65d9d commit 0cc71ef
Show file tree
Hide file tree
Showing 807 changed files with 102,447 additions and 143 deletions.
1 change: 1 addition & 0 deletions cmd/meroxa/root/apps/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func (*Apps) Docs() builder.Docs {

func (*Apps) SubCommands() []*cobra.Command {
return []*cobra.Command{
builder.BuildCobraCommand(&Deploy{}),
builder.BuildCobraCommand(&Run{}),
builder.BuildCobraCommand(&Init{}),
}
Expand Down
162 changes: 161 additions & 1 deletion cmd/meroxa/root/apps/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,40 @@ package apps

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path"
"time"

"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/archive"
"github.com/meroxa/cli/cmd/meroxa/global"
"github.com/meroxa/cli/log"
turbine "github.com/meroxa/turbine/deploy"
)

const (
dockerHubUserNameEnv = "DOCKER_HUB_USERNAME"
dockerHubAccessTokenEnv = "DOCKER_HUB_ACCESS_TOKEN" // nolint:gosec
goLang = "golang"
javaScript = "javascript"
nodeJS = "nodejs"
)

func buildGoApp(ctx context.Context, l log.Logger, appPath, appName string, platform bool) error {
var cmd *exec.Cmd

if appName != "" {
appName = appPath
}

if platform {
cmd = exec.Command("go", "build", "--tags", "platform", "-o", "./"+appPath, "./"+appPath+"/...") //nolint:gosec
cmd = exec.Command("go", "build", "--tags", "platform", "-o", appName, "./...")
} else {
cmd = exec.Command("go", "build", "-o", appName, "./...")
}
Expand All @@ -24,3 +49,138 @@ func buildGoApp(ctx context.Context, l log.Logger, appPath, appName string, plat

return nil
}

func deployApp(ctx context.Context, l log.Logger, appPath, appName, imageName string) error {
l.Info(ctx, "deploying app...\n")

cmd := exec.Command(appPath+"/"+appName, "--deploy", "--imagename", imageName) // nolint:gosec
accessToken, refreshToken, err := global.GetUserToken()
if err != nil {
return err
}
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("ACCESS_TOKEN=%s", accessToken), fmt.Sprintf("REFRESH_TOKEN=%s", refreshToken))

stdout, err := cmd.CombinedOutput()
if err != nil {
l.Errorf(ctx, string(stdout))
return err
}

l.Info(ctx, string(stdout))
l.Info(ctx, "deploy complete!")
return nil
}

func buildImage(ctx context.Context, l log.Logger, pwd, imageName string) error {
l.Infof(ctx, "Building image %q located at %q", imageName, pwd)
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
l.Errorf(ctx, "unable to init docker client; %s", err)
}

// Generate dockerfile
err = turbine.CreateDockerfile(pwd)
if err != nil {
return err
}

// Read local Dockerfile
tar, err := archive.TarWithOptions(pwd, &archive.TarOptions{
Compression: archive.Uncompressed,
ExcludePatterns: []string{".git", "fixtures"},
})
if err != nil {
l.Errorf(ctx, "unable to create tar; %s", err)
}

buildOptions := types.ImageBuildOptions{
Context: tar,
Dockerfile: "Dockerfile",
Remove: true,
Tags: []string{imageName}}

resp, err := cli.ImageBuild(
ctx,
tar,
buildOptions,
)
if err != nil {
l.Errorf(ctx, "unable to build docker image; %s", err)
}
defer func(Body io.ReadCloser) {
err = Body.Close()
if err != nil {
l.Errorf(ctx, "unable to close docker build response body; %s", err)
}
}(resp.Body)
_, err = io.Copy(os.Stdout, resp.Body)
if err != nil {
l.Errorf(ctx, "unable to read image build response; %s", err)
}
return nil
}

func pushImage(l log.Logger, imageName string) error {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return err
}
authConfig := getAuthConfig()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*120) // nolint:gomnd
defer cancel()

l.Infof(ctx, "pushing image %q to container registry", imageName)
opts := types.ImagePushOptions{RegistryAuth: authConfig}
rd, err := cli.ImagePush(ctx, imageName, opts)
if err != nil {
return err
}
defer func(rd io.ReadCloser) {
err = rd.Close()
if err != nil {
l.Error(ctx, err.Error())
}
}(rd)

_, err = io.Copy(os.Stdout, rd)
if err != nil {
return err
}
l.Info(ctx, "image successfully pushed to container registry!")

return nil
}

func getAuthConfig() string {
dhUsername := os.Getenv(dockerHubUserNameEnv)
dhAccessToken := os.Getenv(dockerHubAccessTokenEnv)
authConfig := types.AuthConfig{
Username: dhUsername,
Password: dhAccessToken,
ServerAddress: "https://index.docker.io/v1/",
}
authConfigBytes, _ := json.Marshal(authConfig)
return base64.URLEncoding.EncodeToString(authConfigBytes)
}

func prependAccount(imageName string) string {
account := os.Getenv(dockerHubUserNameEnv)
return account + "/" + imageName
}

func readConfigFile(appPath string) (AppConfig, error) {
var appConfig AppConfig

appConfigPath := path.Join(appPath, "app.json")
appConfigBytes, err := os.ReadFile(appConfigPath)
if err != nil {
return appConfig, fmt.Errorf("%v\n"+
"We couldn't find an app.json file on path %q. Maybe try in another using `--path`", err, appPath)
}
if err := json.Unmarshal(appConfigBytes, &appConfig); err != nil {
return appConfig, err
}

return appConfig, nil
}
166 changes: 166 additions & 0 deletions cmd/meroxa/root/apps/deploy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
Copyright © 2022 Meroxa Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package apps

import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path"

"github.com/meroxa/cli/cmd/meroxa/builder"
"github.com/meroxa/cli/cmd/meroxa/global"
"github.com/meroxa/cli/config"
"github.com/meroxa/cli/log"
)

type Deploy struct {
flags struct {
Path string `long:"path" description:"path to the app directory"`
}

config config.Config
logger log.Logger
}

var (
_ builder.CommandWithDocs = (*Deploy)(nil)
_ builder.CommandWithFlags = (*Deploy)(nil)
_ builder.CommandWithExecute = (*Deploy)(nil)
_ builder.CommandWithLogger = (*Deploy)(nil)
_ builder.CommandWithConfig = (*Deploy)(nil)
)

func (*Deploy) Usage() string {
return "deploy"
}

func (*Deploy) Docs() builder.Docs {
return builder.Docs{
Short: "Deploy a Meroxa Data Application",
Example: "meroxa apps deploy # assumes you run it from the app directory" +
"meroxa apps deploy --path ./my-app",
}
}

func (d *Deploy) Config(cfg config.Config) {
d.config = cfg
}

func (d *Deploy) Flags() []builder.Flag {
return builder.BuildFlags(&d.flags)
}

func (d *Deploy) checkRequiredEnvVars() error {
// TODO: Make sure we could read from either config file or via env vars
v := os.Getenv(dockerHubUserNameEnv)
k := os.Getenv(dockerHubAccessTokenEnv)

if v == "" || k == "" {
return errors.New("both `DOCKER_HUB_USERNAME` and `DOCKER_HUB_ACCESS_TOKEN` are required to be set to deploy your application")
}
return nil
}

func (d *Deploy) getPath() string {
if d.flags.Path != "" {
return d.flags.Path
}
return "."
}

func (d *Deploy) deployGoApp(ctx context.Context) error {
appPath := d.getPath()

appName := path.Base(appPath)
fqImageName := prependAccount(appName)
err := buildImage(ctx, d.logger, appPath, fqImageName)
if err != nil {
d.logger.Errorf(ctx, "unable to build image; %q\n%s", fqImageName, err)
}

err = pushImage(d.logger, fqImageName)
if err != nil {
d.logger.Errorf(ctx, "unable to push image; %q\n%s", fqImageName, err)
}

err = buildGoApp(ctx, d.logger, appPath, appName, true)
if err != nil {
return err
}

// deploy data app
err = deployApp(ctx, d.logger, appPath, appName, fqImageName)
if err != nil {
d.logger.Errorf(ctx, "unable to deploy app; %s", err)
}

return nil
}

func (d *Deploy) deployJSApp(ctx context.Context) error {
cmd := exec.Command("npx", "turbine", "deploy", d.getPath()) // nolint:gosec

accessToken, _, err := global.GetUserToken()
if err != nil {
return err
}
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("MEROXA_ACCESS_TOKEN=%s", accessToken))

stdout, err := cmd.CombinedOutput()
if err != nil {
d.logger.Error(ctx, string(stdout))
return err
}
d.logger.Info(ctx, string(stdout))
return nil
}

func (d *Deploy) Execute(ctx context.Context) error {
err := d.checkRequiredEnvVars()
if err != nil {
return err
}

appPath := d.getPath()
appConfig, err := readConfigFile(appPath)
if err != nil {
return err
}

lang := appConfig.Language

if appConfig.Language == "" {
return fmt.Errorf("`language` should be specified in your app.json")
}

switch lang {
case "go", "golang":
return d.deployGoApp(ctx)
case "js", "javascript", nodeJS:
return d.deployJSApp(ctx)
default:
return fmt.Errorf("language %q not supported. Currently, we support \"javascript\" and \"go\"", lang)
}
}

func (d *Deploy) Logger(logger log.Logger) {
d.logger = logger
}
Loading

0 comments on commit 0cc71ef

Please sign in to comment.