Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

in progress #184

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/config/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ type Command struct {
Ref string
// can be specified only with ref
RefArgs []string
Plugins map[string]CommandPlugin
}

// NewCommand creates new command struct.
Expand All @@ -66,6 +67,7 @@ func NewCommand(name string) Command {
Name: name,
Env: make(map[string]string),
SkipDocopts: false,
Plugins: make(map[string]CommandPlugin),
}
}

Expand Down
5 changes: 4 additions & 1 deletion config/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var (
// COMMANDS is a top-level directive. Includes all commands to run.
COMMANDS = "commands"
SHELL = "shell"
PLUGINS = "plugins"
ENV = "env"
EvalEnv = "eval_env"
MIXINS = "mixins"
Expand All @@ -15,7 +16,7 @@ var (

var (
ValidConfigDirectives = set.NewSet(
COMMANDS, SHELL, ENV, EvalEnv, MIXINS, VERSION, BEFORE,
COMMANDS, SHELL, ENV, EvalEnv, MIXINS, VERSION, BEFORE, PLUGINS,
)
ValidMixinConfigDirectives = set.NewSet(
COMMANDS, ENV, EvalEnv, BEFORE,
Expand All @@ -36,6 +37,7 @@ type Config struct {
isMixin bool // if true, we consider config as mixin and apply different parsing and validation
// absolute path to .lets
DotLetsDir string
Plugins map[string]ConfigPlugin
}

func NewConfig(workDir string, configAbsPath string, dotLetsDir string) *Config {
Expand All @@ -45,6 +47,7 @@ func NewConfig(workDir string, configAbsPath string, dotLetsDir string) *Config
WorkDir: workDir,
FilePath: configAbsPath,
DotLetsDir: dotLetsDir,
Plugins: make(map[string]ConfigPlugin),
}
}

Expand Down
82 changes: 82 additions & 0 deletions config/config/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package config

import (
"bytes"
"encoding/json"
"fmt"
"os"
"os/exec"
)

type ConfigPlugin struct {
Name string
// if Repo not specified, then it is lets own plugins, lets-cli/lets-plugin-<name>
// if Repo in format <repo>, then we append Name to Repo, <repo>/<name> # TODO or maybe <repo>/lets-plugin-<name>
Repo string
Version string
Url string
Bin string
}

type pluginResult struct {
ResultType string `json:"type"`
Result string `json:"result"`
}

func (p ConfigPlugin) Exec(commandPlugin CommandPlugin) error {
config, err := commandPlugin.SerializeConfig()
if err != nil {
return err
}

cmd := exec.Command(
p.Bin,
string(config), // TODo how to encode
) // #nosec G204

var out bytes.Buffer
// TODO how to show plugin logs ?
cmd.Stdout = &out
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
return err
//return &RunErr{err: fmt.Errorf("failed to run child command '%s' from 'depends': %w", r.cmd.Name, err)}
}

// TODO check exit code
output := out.String()
fmt.Printf("output %s", output)

result := pluginResult{}
if err := json.Unmarshal([]byte(output), &result); err != nil {
return err
}
fmt.Printf("result %v", result)

if result.ResultType == "json" {
pluginResponse := map[string]interface{}{}

if err := json.Unmarshal([]byte(result.Result), &pluginResponse); err != nil {
return err
}

fmt.Printf("pluginResponse %v", pluginResponse)
}
return nil
}

// TODO maybe plugin must have lifecycle
type CommandPlugin struct {
Name string
Config map[string]interface{}
}

func (p CommandPlugin) Run(cmd *Command, cfg *Config) error {
plugin := cfg.Plugins[p.Name]
return plugin.Exec(p)
}

func (p CommandPlugin) SerializeConfig() ([]byte, error) {
return json.Marshal(p.Config)
}
12 changes: 12 additions & 0 deletions config/parser/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ var (
DESCRIPTION = "description"
WORKDIR = "work_dir"
SHELL = "shell"
PLUGINS = "plugins"
ENV = "env"
EvalEnv = "eval_env"
OPTIONS = "options"
Expand All @@ -29,6 +30,7 @@ var directives = set.NewSet[string](
DESCRIPTION,
WORKDIR,
SHELL,
PLUGINS,
ENV,
EvalEnv,
OPTIONS,
Expand Down Expand Up @@ -76,6 +78,16 @@ func parseCommand(newCmd *config.Command, rawCommand map[string]interface{}, cfg
}
}

if rawPlugins, ok := rawCommand[PLUGINS]; ok {
plugins, ok := rawPlugins.(map[string]interface{})
if !ok {
return fmt.Errorf("plugins must be a mapping")
}
if err := parsePlugins(plugins, newCmd); err != nil {
return err
}
}

rawEnv := make(map[string]interface{})

if env, ok := rawCommand[ENV]; ok {
Expand Down
46 changes: 46 additions & 0 deletions config/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,16 @@ func parseConfig(rawKeyValue map[string]interface{}, cfg *config.Config) error {
return fmt.Errorf("'shell' field is required")
}

if rawPlugins, ok := rawKeyValue[config.PLUGINS]; ok {
plugins, ok := rawPlugins.(map[string]interface{})
if !ok {
return fmt.Errorf("plugins must be a mapping")
}
if err := parseConfigPlugins(plugins, cfg); err != nil {
return err
}
}

if mixins, ok := rawKeyValue[config.MIXINS]; ok {
mixins, ok := mixins.([]interface{})
if !ok {
Expand Down Expand Up @@ -279,6 +289,42 @@ func parseBefore(before string, cfg *config.Config) error {
return nil
}

func parseConfigPlugins(rawPlugins map[string]interface{}, cfg *config.Config) error {
plugins := make(map[string]config.ConfigPlugin)

for key, value := range rawPlugins {
pluginConfig, ok := value.(map[string]interface{})
if !ok {
// TODO maybe print plugin configuration schema
return fmt.Errorf("plugin %s configuration must be a mapping", key)
}

plugin := config.ConfigPlugin{Name: key}

for configKey, configVal := range pluginConfig {
switch configVal := configVal.(type) {
case string:
switch configKey {
case "version":
plugin.Version = configVal
case "url":
plugin.Url = configVal
case "bin":
plugin.Bin = configVal
case "repo":
plugin.Repo = configVal
}
}
}

plugins[key] = plugin
}

cfg.Plugins = plugins

return nil
}

func parseCommands(cmds map[string]interface{}, cfg *config.Config) ([]config.Command, error) {
var commands []config.Command
for rawName, rawValue := range cmds {
Expand Down
26 changes: 26 additions & 0 deletions config/parser/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package parser

import (
"fmt"

"github.com/lets-cli/lets/config/config"
)

func parsePlugins(rawPlugins map[string]interface{}, newCmd *config.Command) error {
plugins := make(map[string]config.CommandPlugin)

for key, value := range rawPlugins {
// TODO validate if plugin declared here is declared in config at the top
pluginConfig, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("plugin %s configuration must be a mapping", key)
}

plugin := config.CommandPlugin{Name: key, Config: pluginConfig}
plugins[key] = plugin
}

newCmd.Plugins = plugins

return nil
}
28 changes: 25 additions & 3 deletions lets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,40 @@ mixins:
- build.yaml
- -lets.my.yaml

plugins:
docker-push-pull:
repo: lets-cli/lets-plugin-docker-push-pull
# TODO version is hashed for cache
version: v0.0.45
#version: latest
# TODO url is hashed for cache
url: "https://github.com/lets-cli/lets/releases/download/{{.Version}}/lets_{{.Os}}_{{.Arch}}.tar.gz"
# TODO if bin specified, url is ignored
bin: ../lets-plugin-docker-push-pull/docker-push-pull

env:
NAME: "max"
AGE: 27
CURRENT_UID:
sh: echo "`id -u`:`id -g`"
NGINX_DEV:
checksum: [go.mod, go.sum]

eval_env:
CURRENT_UID: echo "`id -u`:`id -g`"
LINT_TAG:
checksum: [docker/lint.Dockerfile]

commands:
build-image:
plugins:
docker-push-pull:
registry_url: "https://registry.evo.dev"
build_context: "/home/max/code/lets-workspace/lets"
dockerfile: docker/lint.Dockerfile
# image: registry.evo.dev/core-team/company-stats/backend
image: lets-lint
tag: "123" # TODO has to resolve env
# tag: ${LINT_TAG} # TODO has to resolve env
target: dev

release:
description: Create tag and push
options: |
Expand Down
8 changes: 8 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/lets-cli/lets/config"
"github.com/lets-cli/lets/env"
"github.com/lets-cli/lets/logging"
"github.com/lets-cli/lets/plugins"
"github.com/lets-cli/lets/runner"
"github.com/lets-cli/lets/workdir"
log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -44,6 +45,13 @@ func main() {
cmd.ConfigErrorCheck(rootCmd, readConfigErr)
}

if len(cfg.Plugins) > 0 {
if err := plugins.Load(cfg); err != nil {
log.Error(err)
os.Exit(1)
}
}

if err := rootCmd.ExecuteContext(ctx); err != nil {
log.Error(err.Error())

Expand Down
63 changes: 63 additions & 0 deletions plugins/load.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package plugins

import (
"fmt"
"strings"

"github.com/lets-cli/lets/config/config"
)

func Load(cfg *config.Config) error {
// 1. check if plugin exist on .lets
// 2. check if version is downloaded in .lets
// 3. downloading progress bar
for _, plugin := range cfg.Plugins {
if plugin.Bin != "" {
// if bin specified, skip downloading new version
// TODO do we need to copypaste binary to .lets ?
continue
}

// TODO validate repo and url
if plugin.Url == "" {
plugin.Url = getDefaultDownloadUrl(plugin)
} else {
plugin.Url = expandUrl(plugin, cfg)
}

// TODO download from url

}
return nil
}

func getDefaultDownloadUrl(plugin config.ConfigPlugin) string {
repo := plugin.Repo
if repo == "" {
repo = fmt.Sprintf("lets-cli/lets-plugin-%s", plugin.Name)
} else if !strings.Contains(repo, "/") {
repo = fmt.Sprintf("%s/lets-plugin-%s", repo, plugin.Name)
}

//https://github.com/lets-cli/lets/releases/download/{{.Version}}/lets_{{.Os}}_{{.Arch}}.tar.gz
os := "linux"
arch := "amd64"
bin := fmt.Sprintf("lets_plugin_%s_%s_%s", plugin.Name, os, arch)
// TODO require bin or tar.gz ?
version := plugin.Version // TODO what if latest ?
return fmt.Sprintf(
"https://github.com/%s/releases/download/%s/%s",
repo, version, bin,
)
}

func expandUrl(plugin config.ConfigPlugin, cfg *config.Config) string {
url := plugin.Url

if strings.Contains(url, "{{.Version}}") {
// TODO well we must use go templates here ))
url = strings.Replace(url, "{{.Version}}", plugin.Version, 1)
}

return url
}
Loading