Skip to content

Commit

Permalink
New action (#77)
Browse files Browse the repository at this point in the history
* Refactor actions, add go func implementation, improve action input.

---------

Co-authored-by: Igor Ignatyev <[email protected]>
  • Loading branch information
lexbritvin and iignatevich authored Jan 16, 2025
1 parent 72650e4 commit 2530e05
Show file tree
Hide file tree
Showing 64 changed files with 2,663 additions and 1,494 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ FIRST_GOPATH:=$(firstword $(subst :, ,$(GOPATH)))

# Build available information.
GIT_HASH:=$(shell git log --format="%h" -n 1 2> /dev/null)
GIT_BRANCH:=$(shell git branch 2> /dev/null | grep '*' | cut -f2 -d' ')
GIT_BRANCH:=$(shell git rev-parse --abbrev-ref HEAD)
APP_VERSION:="$(GIT_BRANCH)-$(GIT_HASH)"
GOPKG:=github.com/launchrctl/launchr

Expand Down
186 changes: 18 additions & 168 deletions app.go
Original file line number Diff line number Diff line change
@@ -1,37 +1,21 @@
package launchr

import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"time"

"github.com/launchrctl/launchr/internal/launchr"
"github.com/launchrctl/launchr/pkg/action"
_ "github.com/launchrctl/launchr/plugins" // include default plugins
)

var (
errDiscoveryTimeout = "action discovery timeout exceeded"
)

// ActionsGroup is a command group definition.
var ActionsGroup = &launchr.CommandGroup{
ID: "actions",
Title: "Actions:",
}

type appImpl struct {
// Cli related.
cmd *Command
flags []string
skipActions bool // skipActions to skip loading if not requested.
reqCmd string // reqCmd to search for the requested command.
cmd *Command
earlyCmd launchr.CmdEarlyParsed

// FS related.
mFS []ManagedFS
Expand All @@ -41,37 +25,7 @@ type appImpl struct {
// Services.
streams Streams
services map[ServiceInfo]Service
actionMngr action.Manager
pluginMngr PluginManager
config Config
}

// getPluginByType returns specific plugins from the app.
func getPluginByType[T Plugin](app *appImpl) []launchr.MapItem[PluginInfo, T] {
// Collect plugins according to their weights.
m := make(map[int][]launchr.MapItem[PluginInfo, T])
cnt := 0
for pi, p := range app.pluginMngr.All() {
p, ok := p.(T)
if ok {
item := launchr.MapItem[PluginInfo, T]{K: pi, V: p}
m[pi.Weight] = append(m[pi.Weight], item)
cnt++
}
}
// Sort weight keys.
weights := make([]int, 0, len(m))
for w := range m {
weights = append(weights, w)
}
sort.Ints(weights)
// Merge all to a sorted list of plugins.
// @todo maybe sort everything on init to optimize.
res := make([]launchr.MapItem[PluginInfo, T], 0, cnt)
for _, w := range weights {
res = append(res, m[w]...)
}
return res
}

func newApp() *appImpl {
Expand All @@ -86,8 +40,8 @@ func (app *appImpl) SetStreams(s Streams) { app.streams = s }
func (app *appImpl) RegisterFS(fs ManagedFS) { app.mFS = append(app.mFS, fs) }
func (app *appImpl) GetRegisteredFS() []ManagedFS { return app.mFS }

func (app *appImpl) GetRootCmd() *Command { return app.cmd }
func (app *appImpl) EarlyParsedFlags() []string { return app.flags }
func (app *appImpl) RootCmd() *Command { return app.cmd }
func (app *appImpl) CmdEarlyParsed() launchr.CmdEarlyParsed { return app.earlyCmd }

func (app *appImpl) AddService(s Service) {
info := s.ServiceInfo()
Expand Down Expand Up @@ -123,30 +77,6 @@ func (app *appImpl) GetService(v any) {
panic(fmt.Sprintf("service %q does not exist", stype))
}

// earlyPeekFlags tries to parse flags early to allow change behavior before full boot.
func (app *appImpl) earlyPeekFlags(c *Command) {
var err error
args := os.Args[1:]
// Parse args with internal tools.
// We can't guess cmd because nothing has been defined yet.
_, app.flags, err = c.Find(args)
if err != nil {
// There shouldn't be an error when parsing a clean root command.
panic(err)
}
// Quick parse arguments to see if a version or help was requested.
for i := 0; i < len(app.flags); i++ {
// Skip discover actions if we check version.
if app.flags[i] == "--version" {
app.skipActions = true
}

if app.reqCmd == "" && !strings.HasPrefix(app.flags[i], "-") {
app.reqCmd = args[i]
}
}
}

// init initializes application and plugins.
func (app *appImpl) init() error {
var err error
Expand All @@ -161,8 +91,7 @@ func (app *appImpl) init() error {
return cmd.Help()
},
}
app.earlyPeekFlags(app.cmd)

app.earlyCmd = launchr.EarlyPeekCommand()
// Set io streams.
app.SetStreams(StandardStreams())
app.cmd.SetIn(app.streams.In())
Expand All @@ -171,125 +100,46 @@ func (app *appImpl) init() error {

// Set working dir and config dir.
app.cfgDir = "." + name
app.workDir, err = filepath.Abs(".")
if err != nil {
return err
}
app.workDir = launchr.MustAbs(".")
actionsPath := launchr.MustAbs(os.Getenv(strings.ToUpper(name + "_ACTIONS_PATH")))
// Initialize managed FS for action discovery.
app.mFS = make([]ManagedFS, 0, 4)
app.RegisterFS(action.NewDiscoveryFS(os.DirFS(app.workDir), app.GetWD()))
app.RegisterFS(action.NewDiscoveryFS(os.DirFS(actionsPath), app.GetWD()))

// Prepare dependencies.
app.services = make(map[ServiceInfo]Service)
app.pluginMngr = launchr.NewPluginManagerWithRegistered()
// @todo consider home dir for global config.
app.config = launchr.ConfigFromFS(os.DirFS(app.cfgDir))
app.actionMngr = action.NewManager(
action.WithDefaultRunEnvironment,
action.WithContainerRunEnvironmentConfig(app.config, name+"_"),
config := launchr.ConfigFromFS(os.DirFS(app.cfgDir))
actionMngr := action.NewManager(
action.WithDefaultRuntime,
action.WithContainerRuntimeConfig(config, name+"_"),
action.WithValueProcessors(),
)

// Register services for other modules.
app.AddService(app.actionMngr)
app.AddService(actionMngr)
app.AddService(app.pluginMngr)
app.AddService(app.config)
app.AddService(config)

// Run OnAppInit hook.
for _, p := range getPluginByType[OnAppInitPlugin](app) {
for _, p := range launchr.GetPluginByType[OnAppInitPlugin](app.pluginMngr) {
if err = p.V.OnAppInit(app); err != nil {
return err
}
}

// Discover actions.
if !app.skipActions {
if err = app.discoverActions(); err != nil {
return err
}
}

return nil
}

func (app *appImpl) discoverActions() (err error) {
var discovered []*action.Action
idp := app.actionMngr.GetActionIDProvider()
// @todo configure timeout from flags
// Define timeout for cases when we may traverse the whole FS, e.g. in / or home.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
for _, p := range getPluginByType[action.DiscoveryPlugin](app) {
for _, regfs := range app.GetRegisteredFS() {
actions, errDis := p.V.DiscoverActions(ctx, regfs, idp)
if errDis != nil {
return errDis
}
discovered = append(discovered, actions...)
}
}
// Failed to discover actions in reasonable time.
if errCtx := ctx.Err(); errCtx != nil {
return errors.New(errDiscoveryTimeout)
}

// Add discovered actions.
for _, a := range discovered {
app.actionMngr.Add(a)
}

// Alter all registered actions.
for _, p := range getPluginByType[action.AlterActionsPlugin](app) {
err = p.V.AlterActions()
if err != nil {
return err
}
}
// @todo maybe cache discovery result for performance.
return err
}

func (app *appImpl) exec() error {
if app.skipActions {
if app.earlyCmd.IsVersion {
app.cmd.SetVersionTemplate(Version().Full())
}
// Check the requested command to see what actions we must actually load.
var actions map[string]*action.Action
if app.reqCmd != "" {
// Check if an alias was provided to find the real action.
app.reqCmd = app.actionMngr.GetIDFromAlias(app.reqCmd)
a, ok := app.actionMngr.Get(app.reqCmd)
if ok {
// Use only the requested action.
actions = map[string]*action.Action{a.ID: a}
} else {
// Action was not requested, no need to load them.
app.skipActions = true
}
} else {
// Load all.
actions = app.actionMngr.All()
}
// Convert actions to cobra commands.
// @todo consider cobra completion and caching between runs.
if !app.skipActions {
if len(actions) > 0 {
app.cmd.AddGroup(ActionsGroup)
}
for _, a := range actions {
cmd, err := action.CobraImpl(a, app.Streams())
if err != nil {
Log().Warn("action was skipped due to error", "action_id", a.ID, "error", err)
Term().Warning().Printfln("Action %q was skipped:\n%v", a.ID, err)
continue
}
cmd.GroupID = ActionsGroup.ID
app.cmd.AddCommand(cmd)
}
return app.cmd.Execute()
}

// Add application commands from plugins.
for _, p := range getPluginByType[CobraPlugin](app) {
for _, p := range launchr.GetPluginByType[CobraPlugin](app.pluginMngr) {
if err := p.V.CobraAddCommands(app.cmd); err != nil {
return err
}
Expand Down
3 changes: 3 additions & 0 deletions example/actions/alias/action.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
action:
title: aliasaction
description: Test alias definition

runtime:
type: container
image: buildargs:latest
alias:
- "alias1"
Expand Down
3 changes: 3 additions & 0 deletions example/actions/arguments/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ action:
description: Option to do something
type: boolean
default: false

runtime:
type: container
image: envvars:latest
build:
context: ./
Expand Down
3 changes: 3 additions & 0 deletions example/actions/buildargs/action.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
action:
title: buildargs
description: Test passing args to Dockerfile

runtime:
type: container
image: buildargs:latest
build:
context: ./
Expand Down
3 changes: 3 additions & 0 deletions example/actions/envvars/action.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
action:
title: envvars
description: Test passing static or dynamic environment variables to container

runtime:
type: container
image: envvars:latest
env:
ACTION_ENV1: value_from_action.yaml # Static value
Expand Down
3 changes: 3 additions & 0 deletions example/actions/extrahosts/action.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
action:
title: extrahosts
description: Test passing additional entries to container's /etc/hosts

runtime:
type: container
image: extrahosts:latest
extra_hosts:
- "host.docker.internal:host-gateway"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ action:
- name: arg2
title: Argument 2
description: Some additional info for arg
options: # TODO Use json schema. By default string. Do not allow complex data.
options:
- name: opt1
title: Option 1
description: Some additional info for option

runtime:
type: container
image: python:3.7-slim
command: python3 %s
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ action:
- name: opt1
title: Option 1
description: Some additional info for option

runtime:
type: container
image: python:3.7-slim
command: python3 %s
20 changes: 18 additions & 2 deletions example/actions/platform/actions/build/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ action:
- name: arg2
title: Argument 2
description: Some additional info for arg
options: # TODO Use json schema. By default string. Do not allow complex data.
options:
- name: opt1
title: Option 1
description: Some additional info for option
default:
default: ""
- name: opt2
title: Option 2
description: Some additional info for option
Expand All @@ -30,6 +30,22 @@ action:
title: Option 4
description: Some additional info for option
type: array
- name: optenum
title: Option 5
type: string
enum: [enum1, enum2]
- name: optarrbool
title: Option 6
type: array
items:
type: boolean
- name: optip
title: Option 7
type: string
format: "ipv4"

runtime:
type: container
# image: python:3.7-slim
image: ubuntu
# command: python3 {{ .opt4 }}
Expand Down
Loading

0 comments on commit 2530e05

Please sign in to comment.