1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-08-06 22:32:51 +02:00

remwork tools

This commit is contained in:
Lee Brown
2019-07-13 20:50:00 -08:00
parent 6607d719d9
commit 91879fe40a
12 changed files with 372 additions and 523 deletions

View File

@ -1,149 +0,0 @@
1. Create new policy `saas-starter-kit-deploy` with the following permissions.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ServiceDeployPermissions",
"Effect": "Allow",
"Action": [
"acm:ListCertificates",
"acm:RequestCertificate",
"acm:DescribeCertificate",
"ec2:DescribeSubnets",
"ec2:DescribeSecurityGroups",
"ec2:CreateSecurityGroup",
"ec2:AuthorizeSecurityGroupIngress",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeVpcs",
"ec2:CreateVpc",
"ec2:CreateSubnet",
"ec2:DescribeVpcs",
"ec2:DescribeInternetGateways",
"ec2:CreateInternetGateway",
"ec2:CreateTags",
"ec2:CreateRouteTable",
"ec2:DescribeRouteTables",
"ec2:CreateRoute",
"ec2:AttachInternetGateway",
"ec2:DescribeAccountAttributes",
"elasticache:DescribeCacheClusters",
"elasticache:CreateCacheCluster",
"elasticache:DescribeCacheParameterGroups",
"elasticache:CreateCacheParameterGroup",
"elasticache:ModifyCacheCluster",
"elasticache:ModifyCacheParameterGroup",
"elasticloadbalancing:DescribeLoadBalancers",
"elasticloadbalancing:CreateLoadBalancer",
"elasticloadbalancing:CreateListener",
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:CreateTargetGroup",
"elasticloadbalancing:DescribeListeners",
"elasticloadbalancing:ModifyTargetGroupAttributes",
"ecs:CreateCluster",
"ecs:CreateService",
"ecs:DeleteService",
"ecs:DescribeClusters",
"ecs:DescribeServices",
"ecs:UpdateService",
"ecs:RegisterTaskDefinition",
"ecs:ListTaskDefinitions",
"ecr:BatchCheckLayerAvailability",
"ecr:BatchDeleteImage",
"ecr:GetAuthorizationToken",
"ecr:DescribeImages",
"ecr:DescribeRepositories",
"ecs:DescribeTasks",
"ecr:CreateRepository",
"ecr:ListImages",
"ecs:ListTasks",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"logs:DescribeLogGroups",
"logs:CreateLogGroup",
"lambda:ListFunctions",
"lambda:CreateFunction",
"lambda:UpdateFunctionCode",
"lambda:UpdateFunctionConfiguration",
"iam:GetRole",
"iam:PassRole",
"iam:CreateRole",
"iam:CreateServiceLinkedRole",
"iam:CreatePolicy",
"iam:PutRolePolicy",
"iam:TagRole",
"iam:AttachRolePolicy",
"iam:ListPolicies",
"iam:GetPolicyVersion",
"iam:CreatePolicyVersion",
"logs:DescribeLogGroups",
"logs:CreateLogGroup",
"logs:DescribeLogStreams",
"logs:CreateExportTask",
"logs:DescribeExportTasks",
"rds:CreateDBCluster",
"rds:CreateDBInstance",
"rds:DescribeDBClusters",
"rds:DescribeDBInstances",
"s3:CreateBucket",
"s3:DeleteObject",
"s3:DeleteObjectVersion",
"s3:GetBucketPublicAccessBlock",
"s3:GetBucketAcl",
"s3:HeadBucket",
"s3:ListObjects",
"s3:ListBucket",
"s3:GetObject",
"s3:PutLifecycleConfiguration",
"s3:PutBucketCORS",
"s3:PutBucketPolicy",
"s3:PutBucketPublicAccessBlock",
"route53:CreateHostedZone",
"route53:ChangeResourceRecordSets",
"route53:ListHostedZones",
"secretsmanager:CreateSecret",
"secretsmanager:ListSecrets",
"secretsmanager:GetSecretValue",
"secretsmanager:UpdateSecret",
"secretsmanager:RestoreSecret",
"secretsmanager:DeleteSecret",
"servicediscovery:ListNamespaces",
"servicediscovery:CreatePrivateDnsNamespace",
"servicediscovery:GetOperation",
"servicediscovery:ListServices",
"servicediscovery:CreateService",
"servicediscovery:GetService"
],
"Resource": "*"
},
{
"Action": "iam:CreateServiceLinkedRole",
"Effect": "Allow",
"Resource": "arn:aws:iam::*:role/aws-service-role/rds.amazonaws.com/AWSServiceRoleForRDS",
"Condition": {
"StringLike": {
"iam:AWSServiceName":"rds.amazonaws.com"
}
}
}
]
}
```
2. Create new user `saas-starter-kit-deploy` with _Programmatic Access_ and _Attach existing policies directly_ with the policy created from step 1 `saas-starter-kit-deploy`
3. Try running the deploy
```bash
go run main.go deploy -service=web-api -env=dev
```
Or
```bash
go run main.go deploy -service=web-api -env=dev -enable_https=true -primary_host=eproc.tech -host_names=www.eproc.tech -host_names=api.eproc.tech -recreate_service=false
```

View File

@ -1,155 +0,0 @@
package devops
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
)
// findProjectGoModFile finds the project root directory from the current working directory.
func findProjectGoModFile() (string, error) {
var err error
projectRoot, err := os.Getwd()
if err != nil {
return "", errors.WithMessage(err, "failed to get current working directory")
}
// Try to find the project root for looking for the go.mod file in a parent directory.
var goModFile string
testDir := projectRoot
for i := 0; i < 3; i++ {
if goModFile != "" {
testDir = filepath.Join(testDir, "../")
}
goModFile = filepath.Join(testDir, "go.mod")
ok, _ := exists(goModFile)
if ok {
projectRoot = testDir
break
}
}
// Verify the go.mod file was found.
ok, err := exists(goModFile)
if err != nil {
return "", errors.WithMessagef(err, "failed to load go.mod for project using project root %s")
} else if !ok {
return "", errors.Errorf("failed to locate project go.mod in project root %s", projectRoot)
}
return goModFile, nil
}
// findServiceDockerFile finds the service directory.
func findServiceDockerFile(projectRoot, targetService string) (string, error) {
checkDirs := []string{
filepath.Join(projectRoot, "cmd", targetService),
filepath.Join(projectRoot, "tools", targetService),
}
var dockerFile string
for _, cd := range checkDirs {
// Check to see if directory contains Dockerfile.
tf := filepath.Join(cd, "Dockerfile")
ok, _ := exists(tf)
if ok {
dockerFile = tf
break
}
}
if dockerFile == "" {
return "", errors.Errorf("failed to locate Dockerfile for service %s", targetService)
}
return dockerFile, nil
}
// getTargetEnv checks for an env var that is prefixed with the current target env.
func getTargetEnv(targetEnv, envName string) string {
k := fmt.Sprintf("%s_%s", strings.ToUpper(targetEnv), envName)
if v := os.Getenv(k); v != "" {
// Set the non prefixed env var with the prefixed value.
os.Setenv(envName, v)
return v
}
return os.Getenv(envName)
}
// loadGoModName parses out the module name from go.mod.
func loadGoModName(goModFile string) (string, error) {
ok, err := exists(goModFile)
if err != nil {
return "", errors.WithMessage(err, "Failed to load go.mod for project")
} else if !ok {
return "", errors.Errorf("Failed to locate project go.mod at %s", goModFile)
}
b, err := ioutil.ReadFile(goModFile)
if err != nil {
return "", errors.WithMessagef(err, "Failed to read go.mod at %s", goModFile)
}
var name string
lines := strings.Split(string(b), "\n")
for _, l := range lines {
if strings.HasPrefix(l, "module ") {
name = strings.TrimSpace(strings.Split(l, " ")[1])
break
}
}
return name, nil
}
// exists returns a bool as to whether a file path exists.
func exists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return true, err
}
/*
type EnvVars []string
// execCmds executes a set of commands.
func execCmds(workDir string, envVars *EnvVars, cmds ...[]string) ([]string, error) {
if envVars == nil {
ev := EnvVars(os.Environ())
envVars = &ev
}
var results []string
for _, cmdVals := range cmds {
cmd := exec.Command(cmdVals[0], cmdVals[1:]...)
cmd.Dir = workDir
cmd.Env = *envVars
out, err := cmd.CombinedOutput()
fmt.Println(string(out ))
if err != nil {
return results, errors.WithMessagef(err, "failed to execute %s\n%s", strings.Join(cmdVals, " "), string(out))
}
results = append(results, string(out))
// Update the current env vars after command has been executed.
ev := EnvVars(cmd.Env)
envVars = &ev
}
return results, nil
}
*/

View File

@ -1,199 +0,0 @@
package devops
import (
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ecr"
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/aws/aws-sdk-go/service/elasticache"
"github.com/aws/aws-sdk-go/service/elbv2"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/rds"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/servicediscovery"
"github.com/iancoleman/strcase"
"github.com/urfave/cli"
)
// ServiceDeployFlags defines the flags used for executing a service deployment.
type ServiceDeployFlags struct {
// Required flags.
ServiceName string `validate:"required" example:"web-api"`
Env string `validate:"oneof=dev stage prod" example:"dev"`
// Optional flags.
EnableHTTPS bool `validate:"omitempty" example:"false"`
ServiceHostPrimary string `validate:"omitempty" example:"example-project.com"`
ServiceHostNames cli.StringSlice `validate:"omitempty" example:"subdomain.example-project.com"`
S3BucketPrivateName string `validate:"omitempty" example:"saas-example-project-private"`
S3BucketPublicName string `validate:"omitempty" example:"saas-example-project-public"`
ProjectRoot string `validate:"omitempty" example:"."`
ProjectName string ` validate:"omitempty" example:"example-project"`
DockerFile string `validate:"omitempty" example:"./cmd/web-api/Dockerfile"`
EnableLambdaVPC bool `validate:"omitempty" example:"false"`
EnableEcsElb bool `validate:"omitempty" example:"false"`
NoBuild bool `validate:"omitempty" example:"false"`
NoDeploy bool `validate:"omitempty" example:"false"`
NoCache bool `validate:"omitempty" example:"false"`
NoPush bool `validate:"omitempty" example:"false"`
RecreateService bool `validate:"omitempty" example:"false"`
}
// serviceDeployRequest defines the details needed to execute a service deployment.
type serviceDeployRequest struct {
ServiceName string `validate:"required"`
ServiceDir string `validate:"required"`
Env string `validate:"oneof=dev stage prod"`
ProjectRoot string `validate:"required"`
ProjectName string `validate:"required"`
DockerFile string `validate:"required"`
GoModFile string `validate:"required"`
GoModName string `validate:"required"`
EnableHTTPS bool `validate:"omitempty"`
ServiceHostPrimary string `validate:"omitempty,required_with=EnableHTTPS,fqdn"`
ServiceHostNames []string `validate:"omitempty,dive,fqdn"`
AwsCreds awsCredentials `validate:"required,dive,required"`
EcrRepositoryName string `validate:"required"`
EcrRepository *ecr.CreateRepositoryInput
EcrRepositoryMaxImages int `validate:"omitempty"`
EcsClusterName string `validate:"required"`
EcsCluster *ecs.CreateClusterInput
EcsServiceName string `validate:"required"`
EcsServiceDesiredCount int64 `validate:"required"`
EcsServiceMinimumHealthyPercent *int64 `validate:"omitempty"`
EcsServiceMaximumPercent *int64 `validate:"omitempty"`
EscServiceHealthCheckGracePeriodSeconds *int64 `validate:"omitempty"`
EcsExecutionRoleName string `validate:"required"`
EcsExecutionRole *iam.CreateRoleInput
EcsExecutionRolePolicyArns []string `validate:"required"`
EcsTaskRoleName string `validate:"required"`
EcsTaskRole *iam.CreateRoleInput
EcsTaskPolicyName string `validate:"required"`
EcsTaskPolicy *iam.CreatePolicyInput
EcsTaskPolicyDocument IamPolicyDocument
Ec2SecurityGroupName string `validate:"required"`
Ec2SecurityGroup *ec2.CreateSecurityGroupInput
CloudWatchLogGroupName string `validate:"required"`
CloudWatchLogGroup *cloudwatchlogs.CreateLogGroupInput
S3BucketTempPrefix string `validate:"required_with=S3BucketPrivateName S3BucketPublicName"`
S3BucketPrivateName string `validate:"omitempty"`
S3BucketPublicName string `validate:"omitempty"`
S3Buckets []S3Bucket
EnableEcsElb bool `validate:"omitempty"`
ElbLoadBalancerName string `validate:"omitempty"`
ElbDeregistrationDelay *int `validate:"omitempty"`
ElbLoadBalancer *elbv2.CreateLoadBalancerInput
ElbTargetGroupName string `validate:"omitempty"`
ElbTargetGroup *elbv2.CreateTargetGroupInput
VpcPublicName string `validate:"omitempty"`
VpcPublic *ec2.CreateVpcInput
VpcPublicSubnets []*ec2.CreateSubnetInput
EnableLambdaVPC bool `validate:"omitempty"`
NoBuild bool `validate:"omitempty"`
NoDeploy bool `validate:"omitempty"`
NoCache bool `validate:"omitempty"`
NoPush bool `validate:"omitempty"`
RecreateService bool `validate:"omitempty"`
SDNamepsace *servicediscovery.CreatePrivateDnsNamespaceInput
SDService *servicediscovery.CreateServiceInput
CacheCluster *elasticache.CreateCacheClusterInput
CacheClusterParameter []*elasticache.ParameterNameValue
DBCluster *rds.CreateDBClusterInput
DBInstance *rds.CreateDBInstanceInput
ReleaseImage string
BuildTags []string
flags ServiceDeployFlags
_awsSession *session.Session
}
type S3Bucket struct {
Name string `validate:"omitempty"`
Input *s3.CreateBucketInput
LifecycleRules []*s3.LifecycleRule
CORSRules []*s3.CORSRule
PublicAccessBlock *s3.PublicAccessBlockConfiguration
Policy string
}
// DB mimics the general info needed for services used to define placeholders.
type DB struct {
Host string
User string
Pass string
Database string
Driver string
DisableTLS bool
}
// projectNameCamel takes a project name and returns the camel cased version.
func (r *serviceDeployRequest) ProjectNameCamel() string {
s := strings.Replace(r.ProjectName, "_", " ", -1)
s = strings.Replace(s, "-", " ", -1)
s = strcase.ToCamel(s)
return s
}
// awsSession returns the current AWS session for the serviceDeployRequest.
func (r *serviceDeployRequest) awsSession() *session.Session {
if r._awsSession == nil {
r._awsSession = r.AwsCreds.Session()
}
return r._awsSession
}
// AwsCredentials defines AWS credentials used for deployment. Unable to use roles when deploying
// using gitlab CI/CD pipeline.
type awsCredentials struct {
AccessKeyID string `validate:"required"`
SecretAccessKey string `validate:"required"`
Region string `validate:"required"`
}
// Session returns a new AWS Session used to access AWS services.
func (creds awsCredentials) Session() *session.Session {
return session.New(
&aws.Config{
Region: aws.String(creds.Region),
Credentials: credentials.NewStaticCredentials(creds.AccessKeyID, creds.SecretAccessKey, ""),
})
}
// IamPolicyDocument defines an AWS IAM policy used for defining access for IAM roles, users, and groups.
type IamPolicyDocument struct {
Version string `json:"Version"`
Statement []IamStatementEntry `json:"Statement"`
}
// IamStatementEntry defines a single statement for an IAM policy.
type IamStatementEntry struct {
Sid string `json:"Sid"`
Effect string `json:"Effect"`
Action []string `json:"Action"`
Resource interface{} `json:"Resource"`
}

File diff suppressed because it is too large Load Diff

View File

@ -1,85 +0,0 @@
// Package retry contains a simple retry mechanism defined by a slice of delay
// times. There are no maximum retries accounted for here. If retries should be
// limited, use a Timeout context to keep from retrying forever. This should
// probably be made into something more robust.
package retry
import (
"context"
"time"
)
// queryPollIntervals is a slice of the delays before re-checking the status on
// an executing query, backing off from a short delay at first. This sequence
// has been selected with Athena queries in mind, which may operate very
// quickly for things like schema manipulation, or which may run for an
// extended period of time, when running an actual data analysis query.
// Long-running queries will exhaust their rapid retries quickly, and fall back
// to checking every few seconds or longer.
var DefaultPollIntervals = []time.Duration{
time.Millisecond,
2 * time.Millisecond,
2 * time.Millisecond,
5 * time.Millisecond,
10 * time.Millisecond,
20 * time.Millisecond,
50 * time.Millisecond,
50 * time.Millisecond,
100 * time.Millisecond,
100 * time.Millisecond,
200 * time.Millisecond,
500 * time.Millisecond,
time.Second,
2 * time.Second,
5 * time.Second,
10 * time.Second,
20 * time.Second,
30 * time.Second,
time.Minute,
}
// delayer keeps track of the current delay between retries.
type delayer struct {
Delays []time.Duration
currentIndex int
}
// Delay returns the current delay duration, and advances the index to the next
// delay defined. If the index has reached the end of the delay slice, then it
// will continue to return the maximum delay defined.
func (d *delayer) Delay() time.Duration {
t := d.Delays[d.currentIndex]
if d.currentIndex < len(d.Delays)-1 {
d.currentIndex++
}
return t
}
// Retry uses a slice of time.Duration interval delays to retry a function
// until it either errors or indicates that it is ready to proceed. If f
// returns true, or an error, the retry loop is broken. Pass a closure as f if
// you need to record a value from the operation that you are performing inside
// f.
func Retry(ctx context.Context, retryIntervals []time.Duration, f func() (bool, error)) (err error) {
if retryIntervals == nil || len(retryIntervals) == 0 {
retryIntervals = DefaultPollIntervals
}
d := delayer{Delays: retryIntervals}
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
ok, err := f()
if err != nil {
return err
}
if ok {
return nil
}
time.Sleep(d.Delay())
}
}
return err
}

View File

@ -1,86 +0,0 @@
package retry
import (
"context"
"errors"
"testing"
"time"
)
var errExpectedFailure = errors.New("expected failure for test purposes")
func TestDelayer(t *testing.T) {
delays := []time.Duration{
time.Millisecond,
2 * time.Millisecond,
4 * time.Millisecond,
10 * time.Millisecond,
}
tt := []struct {
desc string
numRetries int
expDelay time.Duration
}{
{"first try", 0, time.Millisecond},
{"second try", 1, 2 * time.Millisecond},
{"len(delays) try", len(delays) - 1, delays[len(delays)-1]},
{"len(delays) + 1 try", len(delays), delays[len(delays)-1]},
{"len(delays) * 2 try", len(delays) * 2, delays[len(delays)-1]},
}
for _, tc := range tt {
t.Run(tc.desc, func(t *testing.T) {
var (
d = delayer{Delays: delays}
delay time.Duration
)
for i := tc.numRetries + 1; i > 0; i-- {
delay = d.Delay()
}
if delay != tc.expDelay {
t.Fatalf(
"expected delay of %s after %d retries, but got %s",
tc.expDelay, tc.numRetries, delay)
}
})
}
}
func TestRetry(t *testing.T) {
delays := []time.Duration{
time.Millisecond,
2 * time.Millisecond,
3 * time.Millisecond,
}
tt := []struct {
desc string
tries int
success bool
err error
}{
{"first try", 1, true, nil},
{"second try error", 2, false, errExpectedFailure},
{"third try success", 3, true, nil},
}
for _, tc := range tt {
t.Run(tc.desc, func(t *testing.T) {
tries := 0
retryFunc := func() (bool, error) {
tries++
if tries == tc.tries {
return tc.success, tc.err
}
t.Logf("try #%d unsuccessful: trying again up to %d times", tries, tc.tries)
return false, nil
}
err := Retry(context.Background(), delays, retryFunc)
if err != tc.err {
t.Fatalf("expected error %s, but got error %s", err, tc.err)
}
if tries != tc.tries {
t.Fatalf("expected %d tries, but tried %d times", tc.tries, tries)
}
})
}
}

View File

@ -12,7 +12,6 @@ import (
"strings"
"geeks-accelerator/oss/saas-starter-kit/tools/truss/cmd/dbtable2crud"
"geeks-accelerator/oss/saas-starter-kit/tools/truss/cmd/devops"
"github.com/kelseyhightower/envconfig"
"github.com/lib/pq"
_ "github.com/lib/pq"
@ -121,8 +120,6 @@ func main() {
// =========================================================================
// Start Truss
var deployFlags devops.ServiceDeployFlags
app := cli.NewApp()
app.Commands = []cli.Command{
{
@ -208,37 +205,6 @@ func main() {
return dbtable2crud.Run(masterDb, log, cfg.DB.Database, dbTable, modelFile, modelName, templateDir, projectPath, c.Bool("saveChanges"))
},
},
{
Name: "deploy",
Aliases: []string{"serviceDeploy"},
Usage: "-service=web-api -env=dev",
Flags: []cli.Flag{
cli.StringFlag{Name: "service", Usage: "name of cmd", Destination: &deployFlags.ServiceName},
cli.StringFlag{Name: "env", Usage: "dev, stage, or prod", Destination: &deployFlags.Env},
cli.BoolFlag{Name: "enable_https", Usage: "enable HTTPS", Destination: &deployFlags.EnableHTTPS},
cli.StringFlag{Name: "primary_host", Usage: "dev, stage, or prod", Destination: &deployFlags.ServiceHostPrimary},
cli.StringSliceFlag{Name: "host_names", Usage: "dev, stage, or prod", Value: &deployFlags.ServiceHostNames},
cli.StringFlag{Name: "private_bucket", Usage: "dev, stage, or prod", Destination: &deployFlags.S3BucketPrivateName},
cli.StringFlag{Name: "public_bucket", Usage: "dev, stage, or prod", Destination: &deployFlags.S3BucketPublicName},
cli.StringFlag{Name: "dockerfile", Usage: "DockerFile for service", Destination: &deployFlags.DockerFile},
cli.StringFlag{Name: "root", Usage: "project root directory", Destination: &deployFlags.ProjectRoot},
cli.StringFlag{Name: "project", Usage: "name of project", Destination: &deployFlags.ProjectName},
cli.BoolFlag{Name: "elb", Usage: "enable deployed to use Elastic Load Balancer", Destination: &deployFlags.EnableEcsElb},
cli.BoolTFlag{Name: "lambda_vpc", Usage: "deploy lambda behind VPC", Destination: &deployFlags.EnableLambdaVPC},
cli.BoolFlag{Name: "no_build", Usage: "skip build and continue directly to deploy", Destination: &deployFlags.NoBuild},
cli.BoolFlag{Name: "no_deploy", Usage: "skip deploy after build", Destination: &deployFlags.NoDeploy},
cli.BoolFlag{Name: "no_cache", Usage: "skip docker cache", Destination: &deployFlags.NoCache},
cli.BoolFlag{Name: "no_push", Usage: "skip docker push after build", Destination: &deployFlags.NoPush},
cli.BoolFlag{Name: "recreate_service", Usage: "skip docker push after build", Destination: &deployFlags.RecreateService},
},
Action: func(c *cli.Context) error {
req, err := devops.NewServiceDeployRequest(log, deployFlags)
if err != nil {
return err
}
return devops.ServiceDeploy(log, req)
},
},
}
err = app.Run(os.Args)

10
tools/truss/makefile Normal file
View File

@ -0,0 +1,10 @@
SHELL := /bin/bash
install:
go install .
build:
go install .
run:
go build . && ./truss