Skip to content

Commit

Permalink
initial app
Browse files Browse the repository at this point in the history
  • Loading branch information
mashiike committed Jan 27, 2022
1 parent b4c9368 commit ded3448
Show file tree
Hide file tree
Showing 12 changed files with 721 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .github/dependabot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: gomod
directory: "/"
schedule:
interval: weekly
time: "20:00"
open-pull-requests-limit: 10
28 changes: 28 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Release
on:
push:
branches:
- "!**/*"
tags:
- "v*.*.*"

jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.17

- name: Check out code into the Go module directory
uses: actions/checkout@v2

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v1
with:
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23 changes: 23 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Test
on: [push]
jobs:
test:
strategy:
matrix:
go:
- 1.17
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: ${{ matrix.go }}
id: go

- name: Check out code into the Go module directory
uses: actions/checkout@v2

- name: Build & Test
run: |
go test -race ./...
33 changes: 33 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# This is an example goreleaser.yaml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
before:
hooks:
- go mod download
builds:
- env:
- CGO_ENABLED=0
main: ./cmd/flexentry
binary: flexentry
ldflags:
- -s -w
- -X main.Version=v{{.Version}}
goos:
- darwin
- linux
- windows
goarch:
- amd64
- arm64
release:
prerelease: true
archives:
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ .Env.NIGHTLY_VERSION }}"
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
79 changes: 79 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,81 @@
# flexentry
![Latest GitHub release](https://img.shields.io/github/release/mashiike/flexentry.svg)
![Github Actions test](https://github.com/mashiike/flexentry/workflows/Test/badge.svg?branch=main)
[![Go Report Card](https://goreportcard.com/badge/mashiike/flexentry)](https://goreportcard.com/report/mashiike/flexentry) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/mashiike/flexentry/blob/master/LICENSE)

Flexible entry point for Amazon ECS Task and Amazon Lambda container images

## Usage

```Dockerfile
FROM golang:1.17-buster

RUN apt-get update && \
apt-get install -y unzip && \
apt-get clean

RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
unzip awscliv2.zip && \
./aws/install && \
rm -R aws && \
rm awscliv2.zip

ARG FLEXENTRY_VERSION=0.0.0
RUN curl -L https://github.com/mashiike/flexentry/releases/download/v${FLEXENTRY_VERSION}/flexentry_v${FLEXENTRY_VERSION}_linux_amd64.tar.gz | tar zxvf - && \
install flexentry_v${FLEXENTRY_VERSION}_linux_amd64/flexentry /usr/local/bin/

ENTRYPOINT ["flexentry"]
```

Basically, all you have to do is specify the entry point of the container image.

### Run on ECS Task

```json
{
"containerDefinitions": [
{
"command": [
"aws --version"
],
"essential": true,
"image": "<ecr image path>",
"name": "sample-app"
}
],
"cpu": "256",
"executionRoleArn": "arn:aws:iam::012345678910:role/ecsTaskExecutionRole",
"family": "sample-task-definition",
"memory": "512",
"networkMode": "awsvpc",
"requiresCompatibilities": [
"FARGATE"
]
}
```

Decide what to execute with `command`, as in the task definition above.

### Run on Lambda with container image

If the environment variable `FLEXENTRY_COMMAND` is specified, the command will be executed.
Otherwise, the command to be executed will be determined according to the payload of the event.

```json
{
"cmd": "aws --version",
"description": "this is sample"
}
```

If the event payload is a string, it will be interpreted as a command.
Otherwise, by default, it looks at the `cmd` key to decide which command to execute.
To change the key, specify a jq expression for reference with `FLEXENTRY_COMMAND_JQ_EXPR`.
For example `export FLEXENTRY_COMMAND_JQ_EXPR=".command"`

When executed by Amazon Lambda, the event payload is passed directly to the standard input as JSON data.

## LICENSE

MIT

46 changes: 46 additions & 0 deletions cmd/flexentry/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"context"
"log"
"os"
"strings"
"time"

"github.com/fatih/color"
"github.com/fujiwara/logutils"
"github.com/mashiike/flexentry"
)

var (
Version string = "current"
)

func main() {
logLevel := "info"
if l := os.Getenv("FLEXENTRY_LOG_LEVEL"); l != "" {
logLevel = l
}
filter := &logutils.LevelFilter{
Levels: []logutils.LogLevel{"debug", "info", "warn", "error"},
ModifierFuncs: []logutils.ModifierFunc{
logutils.Color(color.FgHiBlack),
nil,
logutils.Color(color.FgYellow),
logutils.Color(color.FgRed, color.BgBlack),
},
MinLevel: logutils.LogLevel(strings.ToLower(logLevel)),
Writer: os.Stderr,
}
log.SetOutput(filter)
log.Println("[debug] flexentry version:", Version)
entrypoint := flexentry.Entrypoint{
Executer: flexentry.NewSSMWrapExecuter(
flexentry.NewShellExecuter(),
time.Minute,
),
}
if err := entrypoint.Run(context.Background()); err != nil {
log.Fatalln("[error] ", err)
}
}
132 changes: 132 additions & 0 deletions executer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package flexentry

import (
"context"
"fmt"
"io"
"log"
"os"
"os/exec"
"strings"
"sync"
"time"

"github.com/handlename/ssmwrap"
)

type Executer interface {
Execute(ctx context.Context, stdin io.Reader, commands ...string) error
}

type ShellExecuter struct {
shell string
shellArgs []string

stdout io.Writer
stderr io.Writer
}

func NewShellExecuter() *ShellExecuter {
return &ShellExecuter{
shell: "sh",
shellArgs: []string{"-c"},
stdout: os.Stdout,
stderr: os.Stderr,
}
}

func (e *ShellExecuter) Execute(ctx context.Context, stdin io.Reader, commands ...string) error {
args := make([]string, 0, len(e.shellArgs)+len(commands))
args = append(args, e.shellArgs...)
args = append(args, strings.Join(commands, " "))

log.Printf("[debug] $%s %s", e.shell, strings.Join(args, " "))
cmd := exec.CommandContext(ctx, e.shell, args...)
cmd.Env = os.Environ()
p, _ := cmd.StdinPipe()
go func() {
defer p.Close()
if stdin == nil {
return
}
if _, err := io.Copy(p, stdin); err != nil {
log.Println("[warn] failed to write stdinPipe:", err)
}
}()
cmd.Stderr = e.stderr
cmd.Stdout = e.stdout
if err := cmd.Run(); err != nil {
return err
}
return nil
}

func (e *ShellExecuter) Clone() *ShellExecuter {
cloned := *e
return &cloned
}

func (e *ShellExecuter) SetShell(shell string, shellArgs []string) *ShellExecuter {
cloned := e.Clone()
cloned.shell = shell
cloned.shellArgs = make([]string, len(shellArgs))
copy(cloned.shellArgs, shellArgs)
return cloned
}

func (e *ShellExecuter) SetOutput(stdout, stderr io.Writer) *ShellExecuter {
cloned := e.Clone()
cloned.stdout = stdout
cloned.stderr = stderr
return cloned
}

type SSMWrapExecuter struct {
Executer

mu sync.Mutex
lastExported time.Time
ssmCacheExpires time.Duration
}

func NewSSMWrapExecuter(executer Executer, cacheExpires time.Duration) *SSMWrapExecuter {
return &SSMWrapExecuter{
Executer: executer,
ssmCacheExpires: cacheExpires,
}
}

func (e *SSMWrapExecuter) exportEnvWithCache() error {
e.mu.Lock()
defer e.mu.Unlock()
if e.lastExported.IsZero() || e.lastExported.Before(time.Now().Add(-1*e.ssmCacheExpires)) {
defer func() {
e.lastExported = time.Now()
}()
return e.exportEnv()
}
log.Printf("[debug] exportEnv skipped. last exported at %s", e.lastExported.Format(time.RFC3339))
return nil
}

func (e *SSMWrapExecuter) exportEnv() error {
if paths := os.Getenv("SSMWRAP_PATHS"); paths == "" {
return nil
} else {
if err := ssmwrap.Export(ssmwrap.ExportOptions{
Paths: strings.Split(paths, ","),
Retries: 3,
}); err != nil {
return fmt.Errorf("failed to fetch values from SSM paths %s: %w", paths, err)
}
log.Printf("[debug] exportEnv from SSMWRAP_PATHS=%s", paths)
}
return nil
}

func (e *SSMWrapExecuter) Execute(ctx context.Context, stdin io.Reader, commands ...string) error {
if err := e.exportEnvWithCache(); err != nil {
return err
}
return e.Executer.Execute(ctx, stdin, commands...)
}
Loading

0 comments on commit ded3448

Please sign in to comment.