🎨 Introduce contextualized logging

This commit is multi-fold as it also refactors internally context and logger
as interfaces so it is easier to plug luet as a library externally.

Introduces a garbage collector (related to #227) but doesn't handle yet
parallelism.

Closes #265
This commit is contained in:
Ettore Di Giacinto
2021-12-17 15:21:03 +01:00
parent fd90e0d627
commit c98f427156
105 changed files with 2672 additions and 1271 deletions

View File

@@ -90,7 +90,7 @@ type Bus struct {
*pluggable.Manager
}
func (b *Bus) Initialize(ctx *types.Context, plugin ...string) {
func (b *Bus) Initialize(ctx types.Context, plugin ...string) {
b.Manager.Load(plugin...).Register()
for _, e := range b.Manager.Events {

View File

@@ -18,7 +18,7 @@ package config_test
import (
config "github.com/mudler/luet/pkg/api/core/config"
"github.com/mudler/luet/pkg/api/core/types"
"github.com/mudler/luet/pkg/api/core/context"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
@@ -29,7 +29,7 @@ var _ = Describe("Config", func() {
Context("Test config protect", func() {
It("Protect1", func() {
ctx := types.NewContext()
ctx := context.NewContext()
files := []string{
"etc/foo/my.conf",
"usr/bin/foo",
@@ -59,7 +59,7 @@ var _ = Describe("Config", func() {
})
It("Protect2", func() {
ctx := types.NewContext()
ctx := context.NewContext()
files := []string{
"etc/foo/my.conf",
@@ -86,7 +86,7 @@ var _ = Describe("Config", func() {
})
It("Protect3: Annotation dir without initial slash", func() {
ctx := types.NewContext()
ctx := context.NewContext()
files := []string{
"etc/foo/my.conf",

View File

@@ -0,0 +1,28 @@
// Copyright © 2021 Ettore Di Giacinto <mudler@mocaccino.org>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see <http://www.gnu.org/licenses/>.
package context_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestContext(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Context Suite")
}

View File

@@ -0,0 +1,159 @@
// Copyright © 2021 Ettore Di Giacinto <mudler@mocaccino.org>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see <http://www.gnu.org/licenses/>.
package context
import (
"context"
"os"
"path/filepath"
fileHelper "github.com/mudler/luet/pkg/helpers/file"
gc "github.com/mudler/luet/pkg/api/core/garbagecollector"
"github.com/mudler/luet/pkg/api/core/logger"
"github.com/mudler/luet/pkg/api/core/types"
"github.com/pkg/errors"
)
type Context struct {
*logger.Logger
context.Context
types.GarbageCollector
Config *types.LuetConfig
NoSpinner bool
annotations map[string]interface{}
}
// SetAnnotation sets generic annotations to hold in a context
func (c *Context) SetAnnotation(s string, i interface{}) {
c.annotations[s] = i
}
// GetAnnotation gets generic annotations to hold in a context
func (c *Context) GetAnnotation(s string) interface{} {
return c.annotations[s]
}
type ContextOption func(c *Context) error
// WithLogger sets the logger
func WithLogger(l *logger.Logger) ContextOption {
return func(c *Context) error {
c.Logger = l
return nil
}
}
// WithConfig sets the luet config
func WithConfig(cc *types.LuetConfig) ContextOption {
return func(c *Context) error {
c.Config = cc
return nil
}
}
// NOTE: GC needs to be instantiated when a new context is created from system TmpDirBase
// WithGarbageCollector sets the Garbage collector for the given context
func WithGarbageCollector(l types.GarbageCollector) ContextOption {
return func(c *Context) error {
if !filepath.IsAbs(l.String()) {
abs, err := fileHelper.Rel2Abs(l.String())
if err != nil {
return errors.Wrap(err, "while converting relative path to absolute path")
}
l = gc.GarbageCollector(abs)
}
c.GarbageCollector = l
return nil
}
}
// NewContext returns a new context.
// It accepts a Garbage collector, a config and a logger as an option
func NewContext(opts ...ContextOption) *Context {
l, _ := logger.New()
d := &Context{
annotations: make(map[string]interface{}),
Logger: l,
GarbageCollector: gc.GarbageCollector(filepath.Join(os.TempDir(), "tmpluet")),
Config: &types.LuetConfig{
ConfigFromHost: true,
Logging: types.LuetLoggingConfig{},
General: types.LuetGeneralConfig{},
System: types.LuetSystemConfig{
DatabasePath: filepath.Join("var", "db"),
PkgsCachePath: filepath.Join("var", "db", "packages"),
},
Solver: types.LuetSolverOptions{},
},
}
for _, o := range opts {
o(d)
}
return d
}
// WithLoggingContext returns a copy of the context with a contextualized logger
func (c *Context) WithLoggingContext(name string) types.Context {
configCopy := *c.Config
configCopy.System = c.Config.System
configCopy.General = c.Config.General
configCopy.Logging = c.Config.Logging
ctx := *c
ctxCopy := &ctx
ctxCopy.Config = &configCopy
ctxCopy.annotations = ctx.annotations
ctxCopy.Logger, _ = c.Logger.Copy(logger.WithContext(name))
return ctxCopy
}
// Copy returns a context copy with a reset logging context
func (c *Context) Copy() types.Context {
return c.WithLoggingContext("")
}
func (c *Context) Warning(mess ...interface{}) {
c.Logger.Warn(mess...)
if c.Config.General.FatalWarns {
os.Exit(2)
}
}
func (c *Context) Warn(mess ...interface{}) {
c.Warning(mess...)
}
func (c *Context) Warnf(t string, mess ...interface{}) {
c.Logger.Warnf(t, mess...)
if c.Config.General.FatalWarns {
os.Exit(2)
}
}
func (c *Context) Warningf(t string, mess ...interface{}) {
c.Warnf(t, mess...)
}
func (c *Context) GetConfig() types.LuetConfig {
return *c.Config
}

View File

@@ -0,0 +1,59 @@
// Copyright © 2021 Ettore Di Giacinto <mudler@mocaccino.org>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see <http://www.gnu.org/licenses/>.
package gc
import (
"io/ioutil"
"os"
)
type GarbageCollector string
func (c GarbageCollector) String() string {
return string(c)
}
func (c GarbageCollector) init() error {
if _, err := os.Stat(string(c)); err != nil {
if os.IsNotExist(err) {
err = os.MkdirAll(string(c), os.ModePerm)
if err != nil {
return err
}
}
}
return nil
}
func (c GarbageCollector) Clean() error {
return os.RemoveAll(string(c))
}
func (c GarbageCollector) TempDir(pattern string) (string, error) {
err := c.init()
if err != nil {
return "", err
}
return ioutil.TempDir(string(c), pattern)
}
func (c GarbageCollector) TempFile(s string) (*os.File, error) {
err := c.init()
if err != nil {
return nil, err
}
return ioutil.TempFile(string(c), s)
}

View File

@@ -18,8 +18,8 @@ package image_test
import (
"path/filepath"
"github.com/mudler/luet/pkg/api/core/context"
. "github.com/mudler/luet/pkg/api/core/image"
"github.com/mudler/luet/pkg/api/core/types"
"github.com/mudler/luet/pkg/helpers/file"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
@@ -27,16 +27,16 @@ import (
var _ = Describe("Cache", func() {
ctx := types.NewContext()
ctx := context.NewContext()
Context("used as k/v store", func() {
cache := &Cache{}
var dir string
BeforeEach(func() {
ctx = types.NewContext()
ctx = context.NewContext()
var err error
dir, err = ctx.Config.System.TempDir("foo")
dir, err = ctx.TempDir("foo")
Expect(err).ToNot(HaveOccurred())
cache = NewCache(dir, 10*1024*1024, 1) // 10MB Cache when upgrading to files. Max volatile memory of 1 row.
})

View File

@@ -20,8 +20,8 @@ import (
"path/filepath"
"runtime"
"github.com/mudler/luet/pkg/api/core/context"
. "github.com/mudler/luet/pkg/api/core/image"
"github.com/mudler/luet/pkg/api/core/types"
"github.com/mudler/luet/pkg/api/core/types/artifact"
"github.com/mudler/luet/pkg/compiler/backend"
"github.com/mudler/luet/pkg/helpers/file"
@@ -32,12 +32,12 @@ import (
var _ = Describe("Create", func() {
Context("Creates an OCI image from a standard tar", func() {
It("creates an image which is loadable", func() {
ctx := types.NewContext()
ctx := context.NewContext()
dst, err := ctx.Config.System.TempFile("dst")
dst, err := ctx.TempFile("dst")
Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(dst.Name())
srcTar, err := ctx.Config.System.TempFile("srcTar")
srcTar, err := ctx.TempFile("srcTar")
Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(srcTar.Name())

View File

@@ -23,8 +23,8 @@ import (
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
daemon "github.com/google/go-containerregistry/pkg/v1/daemon"
"github.com/mudler/luet/pkg/api/core/context"
. "github.com/mudler/luet/pkg/api/core/image"
"github.com/mudler/luet/pkg/api/core/types"
"github.com/mudler/luet/pkg/helpers/file"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
@@ -47,7 +47,7 @@ var _ = Describe("Delta", func() {
})
Context("ExtractDeltaFiles", func() {
ctx := types.NewContext()
ctx := context.NewContext()
var tmpfile *os.File
var ref, ref2 name.Reference
var img, img2 v1.Image
@@ -59,7 +59,7 @@ var _ = Describe("Delta", func() {
img2, _ = daemon.Image(ref2)
BeforeEach(func() {
ctx = types.NewContext()
ctx = context.NewContext()
tmpfile, err = ioutil.TempFile("", "delta")
Expect(err).ToNot(HaveOccurred())

View File

@@ -35,7 +35,7 @@ import (
// considering the added files only, and applies a filter on them based on the regexes
// in the lists.
func ExtractDeltaAdditionsFiles(
ctx *types.Context,
ctx types.Context,
srcimg v1.Image,
includes []string, excludes []string,
) (func(h *tar.Header) (bool, error), error) {
@@ -43,7 +43,7 @@ func ExtractDeltaAdditionsFiles(
includeRegexp := compileRegexes(includes)
excludeRegexp := compileRegexes(excludes)
srcfilesd, err := ctx.Config.System.TempDir("srcfiles")
srcfilesd, err := ctx.TempDir("srcfiles")
if err != nil {
return nil, err
}
@@ -128,7 +128,7 @@ func ExtractDeltaAdditionsFiles(
// It then filters files by an include and exclude list.
// The list can be regexes
func ExtractFiles(
ctx *types.Context,
ctx types.Context,
prefixPath string,
includes []string, excludes []string,
) func(h *tar.Header) (bool, error) {
@@ -193,7 +193,7 @@ func ExtractFiles(
// ExtractReader perform the extracting action over the io.ReadCloser
// it extracts the files over output. Accepts a filter as an option
// and additional containerd Options
func ExtractReader(ctx *types.Context, reader io.ReadCloser, output string, filter func(h *tar.Header) (bool, error), opts ...containerdarchive.ApplyOpt) (int64, string, error) {
func ExtractReader(ctx types.Context, reader io.ReadCloser, output string, filter func(h *tar.Header) (bool, error), opts ...containerdarchive.ApplyOpt) (int64, string, error) {
defer reader.Close()
// If no filter is specified, grab all.
@@ -213,8 +213,8 @@ func ExtractReader(ctx *types.Context, reader io.ReadCloser, output string, filt
}
// Extract is just syntax sugar around ExtractReader. It extracts an image into a dir
func Extract(ctx *types.Context, img v1.Image, filter func(h *tar.Header) (bool, error), opts ...containerdarchive.ApplyOpt) (int64, string, error) {
tmpdiffs, err := ctx.Config.GetSystem().TempDir("extraction")
func Extract(ctx types.Context, img v1.Image, filter func(h *tar.Header) (bool, error), opts ...containerdarchive.ApplyOpt) (int64, string, error) {
tmpdiffs, err := ctx.TempDir("extraction")
if err != nil {
return 0, "", errors.Wrap(err, "Error met while creating tempdir for rootfs")
}
@@ -222,6 +222,6 @@ func Extract(ctx *types.Context, img v1.Image, filter func(h *tar.Header) (bool,
}
// ExtractTo is just syntax sugar around ExtractReader
func ExtractTo(ctx *types.Context, img v1.Image, output string, filter func(h *tar.Header) (bool, error), opts ...containerdarchive.ApplyOpt) (int64, string, error) {
func ExtractTo(ctx types.Context, img v1.Image, output string, filter func(h *tar.Header) (bool, error), opts ...containerdarchive.ApplyOpt) (int64, string, error) {
return ExtractReader(ctx, mutate.Extract(img), output, filter, opts...)
}

View File

@@ -23,8 +23,8 @@ import (
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
daemon "github.com/google/go-containerregistry/pkg/v1/daemon"
"github.com/mudler/luet/pkg/api/core/context"
. "github.com/mudler/luet/pkg/api/core/image"
"github.com/mudler/luet/pkg/api/core/types"
"github.com/mudler/luet/pkg/helpers/file"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
@@ -34,14 +34,14 @@ var _ = Describe("Extract", func() {
Context("extract files from images", func() {
Context("ExtractFiles", func() {
ctx := types.NewContext()
ctx := context.NewContext()
var tmpfile *os.File
var ref name.Reference
var img v1.Image
var err error
BeforeEach(func() {
ctx = types.NewContext()
ctx = context.NewContext()
tmpfile, err = ioutil.TempFile("", "extract")
Expect(err).ToNot(HaveOccurred())

View File

@@ -18,7 +18,7 @@ package image_test
import (
"testing"
"github.com/mudler/luet/pkg/api/core/types"
"github.com/mudler/luet/pkg/api/core/context"
"github.com/mudler/luet/pkg/compiler/backend"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
@@ -26,7 +26,7 @@ import (
func TestMutator(t *testing.T) {
RegisterFailHandler(Fail)
b := backend.NewSimpleDockerBackend(types.NewContext())
b := backend.NewSimpleDockerBackend(context.NewContext())
b.DownloadImage(backend.Options{ImageName: "alpine"})
b.DownloadImage(backend.Options{ImageName: "golang:alpine"})

View File

@@ -0,0 +1,28 @@
// Copyright © 2021 Ettore Di Giacinto <mudler@mocaccino.org>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see <http://www.gnu.org/licenses/>.
package logger_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestAPITypes(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Types Suite")
}

View File

@@ -0,0 +1,367 @@
// Copyright © 2021 Ettore Di Giacinto <mudler@mocaccino.org>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see <http://www.gnu.org/licenses/>.
package logger
import (
"fmt"
"os"
"path"
"regexp"
"runtime"
"strings"
"sync"
log "github.com/ipfs/go-log/v2"
"github.com/kyokomi/emoji"
"github.com/pterm/pterm"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// Logger is the default logger
type Logger struct {
level log.LogLevel
emoji bool
logToFile bool
noSpinner bool
fileLogger *zap.Logger
context string
spinnerLock sync.Mutex
s *pterm.SpinnerPrinter
}
// LogLevel represents a log severity level. Use the package variables as an
// enum.
type LogLevel zapcore.Level
type LoggerOptions func(*Logger) error
var NoSpinner LoggerOptions = func(l *Logger) error {
l.noSpinner = true
return nil
}
func WithLevel(level string) LoggerOptions {
return func(l *Logger) error {
lvl, _ := log.LevelFromString(level) // Defaults to Info
l.level = lvl
if l.level == log.LevelDebug {
pterm.EnableDebugMessages()
}
return nil
}
}
func WithContext(c string) LoggerOptions {
return func(l *Logger) error {
l.context = c
return nil
}
}
func WithFileLogging(p, encoding string) LoggerOptions {
return func(l *Logger) error {
if encoding == "" {
encoding = "console"
}
l.logToFile = true
var err error
cfg := zap.NewProductionConfig()
cfg.OutputPaths = []string{p}
cfg.Level = zap.NewAtomicLevelAt(zapcore.Level(l.level))
cfg.ErrorOutputPaths = []string{}
cfg.Encoding = encoding
cfg.DisableCaller = true
cfg.DisableStacktrace = true
cfg.EncoderConfig.TimeKey = "time"
cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
l.fileLogger, err = cfg.Build()
return err
}
}
var EnableEmoji = func() LoggerOptions {
return func(l *Logger) error {
l.emoji = true
return nil
}
}
func New(opts ...LoggerOptions) (*Logger, error) {
l := &Logger{
level: log.LevelDebug,
s: pterm.DefaultSpinner.WithShowTimer(false).WithRemoveWhenDone(true),
}
for _, o := range opts {
if err := o(l); err != nil {
return nil, err
}
}
return l, nil
}
func (l *Logger) Copy(opts ...LoggerOptions) (*Logger, error) {
c := *l
copy := &c
for _, o := range opts {
if err := o(copy); err != nil {
return nil, err
}
}
return copy, nil
}
func joinMsg(args ...interface{}) (message string) {
for _, m := range args {
message += " " + fmt.Sprintf("%v", m)
}
return
}
func (l *Logger) enabled(lvl log.LogLevel) bool {
return lvl >= l.level
}
var emojiStrip = regexp.MustCompile(`[:][\w]+[:]`)
func (l *Logger) transform(args ...interface{}) (sanitized []interface{}) {
for _, a := range args {
var aString string
// Strip emoji if needed
if l.emoji {
aString = emoji.Sprint(a)
} else {
aString = emojiStrip.ReplaceAllString(joinMsg(a), "")
}
sanitized = append(sanitized, aString)
}
if l.context != "" {
sanitized = append([]interface{}{fmt.Sprintf("(%s)", l.context)}, sanitized...)
}
return
}
func prefixCodeLine(args ...interface{}) []interface{} {
pc, file, line, ok := runtime.Caller(3)
if ok {
args = append([]interface{}{fmt.Sprintf("(%s:#%d:%v)",
path.Base(file), line, runtime.FuncForPC(pc).Name())}, args...)
}
return args
}
func (l *Logger) send(ll log.LogLevel, f string, args ...interface{}) {
if !l.enabled(ll) {
return
}
sanitizedArgs := joinMsg(l.transform(args...)...)
sanitizedF := joinMsg(l.transform(f)...)
formatDefined := f != ""
switch {
case log.LevelDebug == ll && !formatDefined:
pterm.Debug.Println(prefixCodeLine(sanitizedArgs)...)
if l.logToFile {
l.fileLogger.Debug(joinMsg(prefixCodeLine(sanitizedArgs)...))
}
case log.LevelDebug == ll && formatDefined:
pterm.Debug.Printfln(sanitizedF, prefixCodeLine(args...)...)
if l.logToFile {
l.fileLogger.Sugar().Debugf(sanitizedF, prefixCodeLine(args...)...)
}
case log.LevelError == ll && !formatDefined:
pterm.Error.Println(pterm.LightRed(sanitizedArgs))
if l.logToFile {
l.fileLogger.Error(sanitizedArgs)
}
case log.LevelError == ll && formatDefined:
pterm.Error.Printfln(pterm.LightRed(sanitizedF), args...)
if l.logToFile {
l.fileLogger.Sugar().Errorf(sanitizedF, args...)
}
case log.LevelFatal == ll && !formatDefined:
pterm.Error.Println(sanitizedArgs)
if l.logToFile {
l.fileLogger.Error(sanitizedArgs)
}
case log.LevelFatal == ll && formatDefined:
pterm.Error.Printfln(sanitizedF, args...)
if l.logToFile {
l.fileLogger.Sugar().Errorf(sanitizedF, args...)
}
//INFO
case log.LevelInfo == ll && !formatDefined:
pterm.Info.Println(sanitizedArgs)
if l.logToFile {
l.fileLogger.Info(sanitizedArgs)
}
case log.LevelInfo == ll && formatDefined:
pterm.Info.Printfln(sanitizedF, args...)
if l.logToFile {
l.fileLogger.Sugar().Infof(sanitizedF, args...)
}
//WARN
case log.LevelWarn == ll && !formatDefined:
pterm.Warning.Println(sanitizedArgs)
if l.logToFile {
l.fileLogger.Warn(sanitizedArgs)
}
case log.LevelWarn == ll && formatDefined:
pterm.Warning.Printfln(sanitizedF, args...)
if l.logToFile {
l.fileLogger.Sugar().Warnf(sanitizedF, args...)
}
}
}
func (l *Logger) Debug(args ...interface{}) {
l.send(log.LevelDebug, "", args...)
}
func (l *Logger) Error(args ...interface{}) {
l.send(log.LevelError, "", args...)
}
func (l *Logger) Trace(args ...interface{}) {
l.send(log.LevelDebug, "", args...)
}
func (l *Logger) Tracef(t string, args ...interface{}) {
l.send(log.LevelDebug, t, args...)
}
func (l *Logger) Fatal(args ...interface{}) {
l.send(log.LevelFatal, "", args...)
os.Exit(1)
}
func (l *Logger) Info(args ...interface{}) {
l.send(log.LevelInfo, "", args...)
}
func (l *Logger) Success(args ...interface{}) {
l.Info(append([]interface{}{"SUCCESS"}, args...)...)
}
func (l *Logger) Panic(args ...interface{}) {
l.Fatal(args...)
}
func (l *Logger) Warn(args ...interface{}) {
l.send(log.LevelWarn, "", args...)
}
func (l *Logger) Warning(args ...interface{}) {
l.Warn(args...)
}
func (l *Logger) Debugf(f string, args ...interface{}) {
l.send(log.LevelDebug, f, args...)
}
func (l *Logger) Errorf(f string, args ...interface{}) {
l.send(log.LevelError, f, args...)
}
func (l *Logger) Fatalf(f string, args ...interface{}) {
l.send(log.LevelFatal, f, args...)
}
func (l *Logger) Infof(f string, args ...interface{}) {
l.send(log.LevelInfo, f, args...)
}
func (l *Logger) Panicf(f string, args ...interface{}) {
l.Fatalf(joinMsg(f), args...)
}
func (l *Logger) Warnf(f string, args ...interface{}) {
l.send(log.LevelWarn, f, args...)
}
func (l *Logger) Warningf(f string, args ...interface{}) {
l.Warnf(f, args...)
}
func (l *Logger) Ask() bool {
var input string
l.Info("Do you want to continue with this operation? [y/N]: ")
_, err := fmt.Scanln(&input)
if err != nil {
return false
}
input = strings.ToLower(input)
if input == "y" || input == "yes" {
return true
}
return false
}
// Spinner starts the spinner
func (l *Logger) Spinner() {
if !IsTerminal() || l.noSpinner {
return
}
l.spinnerLock.Lock()
defer l.spinnerLock.Unlock()
if l.s != nil && !l.s.IsActive {
l.s, _ = l.s.Start()
}
}
func (l *Logger) Screen(text string) {
pterm.DefaultHeader.WithBackgroundStyle(pterm.NewStyle(pterm.BgLightBlue)).WithMargin(2).Println(text)
}
func (l *Logger) SpinnerText(suffix, prefix string) {
if !IsTerminal() || l.noSpinner {
return
}
l.spinnerLock.Lock()
defer l.spinnerLock.Unlock()
if l.level == log.LevelDebug {
fmt.Printf("%s %s\n",
suffix, prefix,
)
} else {
l.s.UpdateText(suffix + prefix)
}
}
func (l *Logger) SpinnerStop() {
if !IsTerminal() || l.noSpinner {
return
}
l.spinnerLock.Lock()
defer l.spinnerLock.Unlock()
if l.s != nil {
l.s.Success()
}
}

View File

@@ -13,7 +13,7 @@
// You should have received a copy of the GNU General Public License along
// with this program; if not, see <http://www.gnu.org/licenses/>.
package types_test
package logger_test
import (
"io"
@@ -21,7 +21,8 @@ import (
"os"
"github.com/gookit/color"
types "github.com/mudler/luet/pkg/api/core/types"
"github.com/mudler/luet/pkg/api/core/logger"
. "github.com/mudler/luet/pkg/api/core/logger"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
@@ -44,30 +45,11 @@ func captureStdout(f func(w io.Writer)) string {
}
var _ = Describe("Context and logging", func() {
ctx := types.NewContext()
BeforeEach(func() {
ctx = types.NewContext()
})
Context("LogLevel", func() {
It("converts it correctly to number and zaplog", func() {
Expect(types.ErrorLevel.ToNumber()).To(Equal(0))
Expect(types.InfoLevel.ToNumber()).To(Equal(2))
Expect(types.WarningLevel.ToNumber()).To(Equal(1))
Expect(types.LogLevel("foo").ToNumber()).To(Equal(3))
Expect(types.WarningLevel.ZapLevel().String()).To(Equal("warn"))
Expect(types.InfoLevel.ZapLevel().String()).To(Equal("info"))
Expect(types.ErrorLevel.ZapLevel().String()).To(Equal("error"))
Expect(types.FatalLevel.ZapLevel().String()).To(Equal("fatal"))
Expect(types.LogLevel("foo").ZapLevel().String()).To(Equal("debug"))
})
})
Context("Context", func() {
It("detect if is a terminal", func() {
Expect(captureStdout(func(w io.Writer) {
_, _, err := ctx.GetTerminalSize()
_, _, err := GetTerminalSize()
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal("size not detectable"))
os.Stdout.Write([]byte(err.Error()))
@@ -75,47 +57,71 @@ var _ = Describe("Context and logging", func() {
})
It("respects loglevel", func() {
ctx.Config.GetGeneral().Debug = false
l, err := New(WithLevel("info"))
Expect(err).ToNot(HaveOccurred())
Expect(captureStdout(func(w io.Writer) {
ctx.Debug("")
l.Debug("")
})).To(Equal(""))
ctx.Config.GetGeneral().Debug = true
l, err = New(WithLevel("debug"))
Expect(err).ToNot(HaveOccurred())
Expect(captureStdout(func(w io.Writer) {
ctx.Debug("foo")
l.Debug("foo")
})).To(ContainSubstring("foo"))
})
It("logs with context", func() {
l, err := New(WithLevel("debug"), WithContext("foo"))
Expect(err).ToNot(HaveOccurred())
Expect(captureStdout(func(w io.Writer) {
l.Debug("bar")
})).To(ContainSubstring("(foo) bar"))
})
It("returns copies with logged context", func() {
l, err := New(WithLevel("debug"))
l, _ = l.Copy(logger.WithContext("bazzz"))
Expect(err).ToNot(HaveOccurred())
Expect(captureStdout(func(w io.Writer) {
l.Debug("bar")
})).To(ContainSubstring("(bazzz) bar"))
})
It("logs to file", func() {
ctx.NoColor()
t, err := ioutil.TempFile("", "tree")
Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(t.Name()) // clean up
ctx.Config.GetLogging().EnableLogFile = true
ctx.Config.GetLogging().Path = t.Name()
ctx.Init()
l, err := New(WithLevel("debug"), WithFileLogging(t.Name(), ""))
Expect(err).ToNot(HaveOccurred())
// ctx.Init()
Expect(captureStdout(func(w io.Writer) {
ctx.Info("foot")
l.Info("foot")
})).To(And(ContainSubstring("INFO"), ContainSubstring("foot")))
Expect(captureStdout(func(w io.Writer) {
ctx.Success("test")
l.Success("test")
})).To(And(ContainSubstring("SUCCESS"), ContainSubstring("test")))
Expect(captureStdout(func(w io.Writer) {
ctx.Error("foobar")
l.Error("foobar")
})).To(And(ContainSubstring("ERROR"), ContainSubstring("foobar")))
Expect(captureStdout(func(w io.Writer) {
ctx.Warning("foowarn")
l.Warning("foowarn")
})).To(And(ContainSubstring("WARNING"), ContainSubstring("foowarn")))
l, err := ioutil.ReadFile(t.Name())
ll, err := ioutil.ReadFile(t.Name())
Expect(err).ToNot(HaveOccurred())
logs := string(l)
logs := string(ll)
Expect(logs).To(ContainSubstring("foot"))
Expect(logs).To(ContainSubstring("test"))
Expect(logs).To(ContainSubstring("foowarn"))

View File

@@ -0,0 +1,43 @@
// Copyright © 2021 Ettore Di Giacinto <mudler@mocaccino.org>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see <http://www.gnu.org/licenses/>.
package logger
import (
"errors"
"os"
"github.com/mattn/go-isatty"
"golang.org/x/term"
)
func IsTerminal() bool {
return isatty.IsTerminal(os.Stdout.Fd())
}
// GetTerminalSize returns the width and the height of the active terminal.
func GetTerminalSize() (width, height int, err error) {
w, h, err := term.GetSize(int(os.Stdout.Fd()))
if w <= 0 {
w = 0
}
if h <= 0 {
h = 0
}
if err != nil {
err = errors.New("size not detectable")
}
return w, h, err
}

View File

@@ -41,7 +41,7 @@ import (
bus "github.com/mudler/luet/pkg/api/core/bus"
config "github.com/mudler/luet/pkg/api/core/config"
"github.com/mudler/luet/pkg/api/core/image"
types "github.com/mudler/luet/pkg/api/core/types"
"github.com/mudler/luet/pkg/api/core/types"
backend "github.com/mudler/luet/pkg/compiler/backend"
compression "github.com/mudler/luet/pkg/compiler/types/compression"
compilerspec "github.com/mudler/luet/pkg/compiler/types/spec"
@@ -70,7 +70,7 @@ type PackageArtifact struct {
Runtime *pkg.DefaultPackage `json:"runtime,omitempty"`
}
func ImageToArtifact(ctx *types.Context, img v1.Image, t compression.Implementation, output string, filter func(h *tar.Header) (bool, error)) (*PackageArtifact, error) {
func ImageToArtifact(ctx types.Context, img v1.Image, t compression.Implementation, output string, filter func(h *tar.Header) (bool, error)) (*PackageArtifact, error) {
_, tmpdiffs, err := image.Extract(ctx, img, filter)
if err != nil {
return nil, errors.Wrap(err, "Error met while creating tempdir for rootfs")
@@ -176,12 +176,12 @@ func (a *PackageArtifact) GetFileName() string {
}
// CreateArtifactForFile creates a new artifact from the given file
func CreateArtifactForFile(ctx *types.Context, s string, opts ...func(*PackageArtifact)) (*PackageArtifact, error) {
func CreateArtifactForFile(ctx types.Context, s string, opts ...func(*PackageArtifact)) (*PackageArtifact, error) {
if _, err := os.Stat(s); os.IsNotExist(err) {
return nil, errors.Wrap(err, "artifact path doesn't exist")
}
fileName := path.Base(s)
archive, err := ctx.Config.GetSystem().TempDir("archive")
archive, err := ctx.TempDir("archive")
if err != nil {
return nil, errors.Wrap(err, "error met while creating tempdir for "+s)
}
@@ -191,7 +191,7 @@ func CreateArtifactForFile(ctx *types.Context, s string, opts ...func(*PackageAr
return nil, errors.Wrapf(err, "error while copying %s to %s", s, dst)
}
artifact, err := ctx.Config.GetSystem().TempDir("artifact")
artifact, err := ctx.TempDir("artifact")
if err != nil {
return nil, errors.Wrap(err, "error met while creating tempdir for "+s)
}
@@ -210,9 +210,9 @@ type ImageBuilder interface {
}
// GenerateFinalImage takes an artifact and builds a Docker image with its content
func (a *PackageArtifact) GenerateFinalImage(ctx *types.Context, imageName string, b ImageBuilder, keepPerms bool) error {
func (a *PackageArtifact) GenerateFinalImage(ctx types.Context, imageName string, b ImageBuilder, keepPerms bool) error {
tempimage, err := ctx.Config.GetSystem().TempFile("tempimage")
tempimage, err := ctx.TempFile("tempimage")
if err != nil {
return errors.Wrap(err, "error met while creating tempdir for "+a.Path)
}
@@ -428,7 +428,7 @@ func replaceFileTarWrapper(dst string, inputTarStream io.ReadCloser, mods []stri
return pipeReader
}
func tarModifierWrapperFunc(ctx *types.Context) func(dst, path string, header *tar.Header, content io.Reader) (*tar.Header, []byte, error) {
func tarModifierWrapperFunc(ctx types.Context) func(dst, path string, header *tar.Header, content io.Reader) (*tar.Header, []byte, error) {
return func(dst, path string, header *tar.Header, content io.Reader) (*tar.Header, []byte, error) {
// If the destination path already exists I rename target file name with postfix.
var destPath string
@@ -495,10 +495,10 @@ func tarModifierWrapperFunc(ctx *types.Context) func(dst, path string, header *t
}
}
func (a *PackageArtifact) GetProtectFiles(ctx *types.Context) (res []string) {
func (a *PackageArtifact) GetProtectFiles(ctx types.Context) (res []string) {
annotationDir := ""
if !ctx.Config.ConfigProtectSkip {
if !ctx.GetConfig().ConfigProtectSkip {
// a.CompileSpec could be nil when artifact.Unpack is used for tree tarball
if a.CompileSpec != nil &&
@@ -511,7 +511,7 @@ func (a *PackageArtifact) GetProtectFiles(ctx *types.Context) (res []string) {
// TODO: check if skip this if we have a.CompileSpec nil
cp := config.NewConfigProtect(annotationDir)
cp.Map(a.Files, ctx.Config.GetConfigProtectConfFiles())
cp.Map(a.Files, ctx.GetConfig().ConfigProtectConfFiles)
// NOTE: for unpack we need files path without initial /
res = cp.GetProtectFiles(false)
@@ -521,7 +521,7 @@ func (a *PackageArtifact) GetProtectFiles(ctx *types.Context) (res []string) {
}
// Unpack Untar and decompress (TODO) to the given path
func (a *PackageArtifact) Unpack(ctx *types.Context, dst string, keepPerms bool) error {
func (a *PackageArtifact) Unpack(ctx types.Context, dst string, keepPerms bool) error {
if !strings.HasPrefix(dst, string(os.PathSeparator)) {
return errors.New("destination must be an absolute path")

View File

@@ -20,10 +20,9 @@ import (
"os"
"path/filepath"
"github.com/mudler/luet/pkg/api/core/context"
"github.com/mudler/luet/pkg/api/core/image"
"github.com/mudler/luet/pkg/api/core/types"
. "github.com/mudler/luet/pkg/api/core/types/artifact"
. "github.com/mudler/luet/pkg/compiler/backend"
backend "github.com/mudler/luet/pkg/compiler/backend"
compression "github.com/mudler/luet/pkg/compiler/types/compression"
"github.com/mudler/luet/pkg/compiler/types/options"
@@ -39,7 +38,7 @@ import (
var _ = Describe("Artifact", func() {
Context("Simple package build definition", func() {
ctx := types.NewContext()
ctx := context.NewContext()
It("Generates a verified delta", func() {
generalRecipe := tree.NewGeneralRecipe(pkg.NewInMemoryDatabase(false))
@@ -49,7 +48,7 @@ var _ = Describe("Artifact", func() {
Expect(len(generalRecipe.GetDatabase().GetPackages())).To(Equal(1))
cc := NewLuetCompiler(nil, generalRecipe.GetDatabase(), options.WithContext(types.NewContext()))
cc := NewLuetCompiler(nil, generalRecipe.GetDatabase(), options.WithContext(context.NewContext()))
lspec, err := cc.FromPackage(&pkg.DefaultPackage{Name: "enman", Category: "app-admin", Version: "1.4.0"})
Expect(err).ToNot(HaveOccurred())
@@ -83,7 +82,7 @@ WORKDIR /luetbuild
ENV PACKAGE_NAME=enman
ENV PACKAGE_VERSION=1.4.0
ENV PACKAGE_CATEGORY=app-admin`))
b := NewSimpleDockerBackend(ctx)
b := backend.NewSimpleDockerBackend(ctx)
opts := backend.Options{
ImageName: "luet/base",
SourcePath: tmpdir,
@@ -120,7 +119,7 @@ RUN echo bar > /test2`))
})
It("Generates packages images", func() {
b := NewSimpleDockerBackend(ctx)
b := backend.NewSimpleDockerBackend(ctx)
imageprefix := "foo/"
testString := []byte(`funky test data`)
@@ -177,7 +176,7 @@ RUN echo bar > /test2`))
})
It("Generates empty packages images", func() {
b := NewSimpleDockerBackend(ctx)
b := backend.NewSimpleDockerBackend(ctx)
imageprefix := "foo/"
tmpdir, err := ioutil.TempDir(os.TempDir(), "artifact")

View File

@@ -20,7 +20,7 @@ import (
"os"
"path/filepath"
types "github.com/mudler/luet/pkg/api/core/types"
"github.com/mudler/luet/pkg/api/core/context"
. "github.com/mudler/luet/pkg/api/core/types/artifact"
compilerspec "github.com/mudler/luet/pkg/compiler/types/spec"
fileHelper "github.com/mudler/luet/pkg/helpers/file"
@@ -59,7 +59,7 @@ var _ = Describe("Cache", func() {
Expect(err).ToNot(HaveOccurred())
b := NewPackageArtifact(path)
ctx := types.NewContext()
ctx := context.NewContext()
err = b.Unpack(ctx, tmpdir, false)
Expect(err).ToNot(HaveOccurred())

View File

@@ -45,7 +45,7 @@ type HashOptions struct {
func (c Checksums) List() (res [][]string) {
keys := make([]string, 0)
for k, _ := range c {
for k := range c {
keys = append(keys, k)
}
sort.Strings(keys)

View File

@@ -1,6 +1,4 @@
// Copyright © 2019 Ettore Di Giacinto <mudler@gentoo.org>
// Daniele Rondina <geaaru@sabayonlinux.org>
// 2021 Ettore Di Giacinto <mudler@mocaccino.org>
// Copyright © 2019-2021 Ettore Di Giacinto <mudler@gentoo.org>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@@ -39,19 +37,22 @@ var AvailableResolvers = strings.Join([]string{solver.QLearningResolverType}, "
type LuetLoggingConfig struct {
// Path of the logfile
Path string `mapstructure:"path"`
Path string `yaml:"path" mapstructure:"path"`
// Enable/Disable logging to file
EnableLogFile bool `mapstructure:"enable_logfile"`
EnableLogFile bool `yaml:"enable_logfile" mapstructure:"enable_logfile"`
// Enable JSON format logging in file
JsonFormat bool `mapstructure:"json_format"`
JsonFormat bool `yaml:"json_format" mapstructure:"json_format"`
// Log level
Level LogLevel `mapstructure:"level"`
Level string `yaml:"level" mapstructure:"level"`
// Enable emoji
EnableEmoji bool `mapstructure:"enable_emoji"`
EnableEmoji bool `yaml:"enable_emoji" mapstructure:"enable_emoji"`
// Enable/Disable color in logging
Color bool `mapstructure:"color"`
Color bool `yaml:"color" mapstructure:"color"`
// NoSpinner disable spinner
NoSpinner bool `yaml:"no_spinner" mapstructure:"no_spinner"`
}
type LuetGeneralConfig struct {
@@ -108,8 +109,42 @@ type LuetSystemConfig struct {
TmpDirBase string `yaml:"tmpdir_base" mapstructure:"tmpdir_base"`
}
func (s *LuetSystemConfig) SetRootFS(path string) error {
p, err := fileHelper.Rel2Abs(path)
// Init reads the config and replace user-defined paths with
// absolute paths where necessary, and construct the paths for the cache
// and database on the real system
func (c *LuetConfig) Init() error {
if err := c.System.init(); err != nil {
return err
}
if err := c.loadConfigProtect(); err != nil {
return err
}
// Load repositories
if err := c.loadRepositories(); err != nil {
return err
}
return nil
}
func (s *LuetSystemConfig) init() error {
if err := s.setRootfs(); err != nil {
return err
}
if err := s.setDBPath(); err != nil {
return err
}
s.setCachePath()
return nil
}
func (s *LuetSystemConfig) setRootfs() error {
p, err := fileHelper.Rel2Abs(s.Rootfs)
if err != nil {
return err
}
@@ -118,9 +153,8 @@ func (s *LuetSystemConfig) SetRootFS(path string) error {
return nil
}
func (sc *LuetSystemConfig) GetRepoDatabaseDirPath(name string) string {
dbpath := filepath.Join(sc.Rootfs, sc.DatabasePath)
dbpath = filepath.Join(dbpath, "repos/"+name)
func (sc LuetSystemConfig) GetRepoDatabaseDirPath(name string) string {
dbpath := filepath.Join(sc.DatabasePath, "repos/"+name)
err := os.MkdirAll(dbpath, os.ModePerm)
if err != nil {
panic(err)
@@ -128,45 +162,48 @@ func (sc *LuetSystemConfig) GetRepoDatabaseDirPath(name string) string {
return dbpath
}
func (sc *LuetSystemConfig) GetSystemRepoDatabaseDirPath() string {
func (sc *LuetSystemConfig) setDBPath() error {
dbpath := filepath.Join(sc.Rootfs,
sc.DatabasePath)
err := os.MkdirAll(dbpath, os.ModePerm)
if err != nil {
panic(err)
return err
}
return dbpath
sc.DatabasePath = dbpath
return nil
}
func (sc *LuetSystemConfig) GetSystemPkgsCacheDirPath() (p string) {
func (sc *LuetSystemConfig) setCachePath() {
var cachepath string
if sc.PkgsCachePath != "" {
cachepath = sc.PkgsCachePath
if !filepath.IsAbs(cachepath) {
cachepath = filepath.Join(sc.DatabasePath, sc.PkgsCachePath)
os.MkdirAll(cachepath, os.ModePerm)
} else {
cachepath = sc.PkgsCachePath
}
} else {
// Create dynamic cache for test suites
cachepath, _ = ioutil.TempDir(os.TempDir(), "cachepkgs")
}
if filepath.IsAbs(cachepath) {
p = cachepath
} else {
p = filepath.Join(sc.GetSystemRepoDatabaseDirPath(), cachepath)
}
sc.PkgsCachePath = cachepath // Be consistent with the path we set
return
}
func (sc *LuetSystemConfig) GetRootFsAbs() (string, error) {
return filepath.Abs(sc.Rootfs)
}
type LuetKV struct {
type FinalizerEnv struct {
Key string `json:"key" yaml:"key" mapstructure:"key"`
Value string `json:"value" yaml:"value" mapstructure:"value"`
}
type Finalizers []FinalizerEnv
func (f Finalizers) Slice() (sl []string) {
for _, kv := range f {
sl = append(sl, fmt.Sprintf("%s=%s", kv.Key, kv.Value))
}
return
}
type LuetConfig struct {
Logging LuetLoggingConfig `yaml:"logging,omitempty" mapstructure:"logging"`
General LuetGeneralConfig `yaml:"general,omitempty" mapstructure:"general"`
@@ -179,16 +216,16 @@ type LuetConfig struct {
ConfigFromHost bool `yaml:"config_from_host,omitempty" mapstructure:"config_from_host"`
SystemRepositories LuetRepositories `yaml:"repositories,omitempty" mapstructure:"repositories"`
FinalizerEnvs []LuetKV `json:"finalizer_envs,omitempty" yaml:"finalizer_envs,omitempty" mapstructure:"finalizer_envs,omitempty"`
FinalizerEnvs Finalizers `json:"finalizer_envs,omitempty" yaml:"finalizer_envs,omitempty" mapstructure:"finalizer_envs,omitempty"`
ConfigProtectConfFiles []config.ConfigProtectConfFile `yaml:"-" mapstructure:"-"`
}
func (c *LuetConfig) GetSystemDB() pkg.PackageDatabase {
switch c.GetSystem().DatabaseEngine {
switch c.System.DatabaseEngine {
case "boltdb":
return pkg.NewBoltDatabase(
filepath.Join(c.GetSystem().GetSystemRepoDatabaseDirPath(), "luet.db"))
filepath.Join(c.System.DatabasePath, "luet.db"))
default:
return pkg.NewInMemoryDatabase(true)
}
@@ -198,83 +235,30 @@ func (c *LuetConfig) AddSystemRepository(r LuetRepository) {
c.SystemRepositories = append(c.SystemRepositories, r)
}
func (c *LuetConfig) GetFinalizerEnvsMap() map[string]string {
ans := make(map[string]string)
for _, kv := range c.FinalizerEnvs {
ans[kv.Key] = kv.Value
}
return ans
}
func (c *LuetConfig) SetFinalizerEnv(k, v string) {
keyPresent := false
envs := []LuetKV{}
envs := []FinalizerEnv{}
for _, kv := range c.FinalizerEnvs {
if kv.Key == k {
keyPresent = true
envs = append(envs, LuetKV{Key: kv.Key, Value: v})
envs = append(envs, FinalizerEnv{Key: kv.Key, Value: v})
} else {
envs = append(envs, kv)
}
}
if !keyPresent {
envs = append(envs, LuetKV{Key: k, Value: v})
envs = append(envs, FinalizerEnv{Key: k, Value: v})
}
c.FinalizerEnvs = envs
}
func (c *LuetConfig) GetFinalizerEnvs() []string {
ans := []string{}
for _, kv := range c.FinalizerEnvs {
ans = append(ans, fmt.Sprintf("%s=%s", kv.Key, kv.Value))
}
return ans
}
func (c *LuetConfig) GetFinalizerEnv(k string) (string, error) {
keyNotPresent := true
ans := ""
for _, kv := range c.FinalizerEnvs {
if kv.Key == k {
keyNotPresent = false
ans = kv.Value
}
}
if keyNotPresent {
return "", errors.New("Finalizer key " + k + " not found")
}
return ans, nil
}
func (c *LuetConfig) GetLogging() *LuetLoggingConfig {
return &c.Logging
}
func (c *LuetConfig) GetGeneral() *LuetGeneralConfig {
return &c.General
}
func (c *LuetConfig) GetSystem() *LuetSystemConfig {
return &c.System
}
func (c *LuetConfig) GetSolverOptions() *LuetSolverOptions {
return &c.Solver
}
func (c *LuetConfig) YAML() ([]byte, error) {
return yaml.Marshal(c)
}
func (c *LuetConfig) GetConfigProtectConfFiles() []config.ConfigProtectConfFile {
return c.ConfigProtectConfFiles
}
func (c *LuetConfig) AddConfigProtectConfFile(file *config.ConfigProtectConfFile) {
func (c *LuetConfig) addProtectFile(file *config.ConfigProtectConfFile) {
if c.ConfigProtectConfFiles == nil {
c.ConfigProtectConfFiles = []config.ConfigProtectConfFile{*file}
} else {
@@ -282,28 +266,21 @@ func (c *LuetConfig) AddConfigProtectConfFile(file *config.ConfigProtectConfFile
}
}
func (c *LuetConfig) LoadRepositories(ctx *Context) error {
func (c *LuetConfig) loadRepositories() error {
var regexRepo = regexp.MustCompile(`.yml$|.yaml$`)
var err error
rootfs := ""
// Respect the rootfs param on read repositories
if !c.ConfigFromHost {
rootfs, err = c.GetSystem().GetRootFsAbs()
if err != nil {
return err
}
rootfs = c.System.Rootfs
}
for _, rdir := range c.RepositoriesConfDir {
rdir = filepath.Join(rootfs, rdir)
ctx.Debug("Parsing Repository Directory", rdir, "...")
files, err := ioutil.ReadDir(rdir)
if err != nil {
ctx.Debug("Skip dir", rdir, ":", err.Error())
continue
}
@@ -313,27 +290,20 @@ func (c *LuetConfig) LoadRepositories(ctx *Context) error {
}
if !regexRepo.MatchString(file.Name()) {
ctx.Debug("File", file.Name(), "skipped.")
continue
}
content, err := ioutil.ReadFile(path.Join(rdir, file.Name()))
if err != nil {
ctx.Warning("On read file", file.Name(), ":", err.Error())
ctx.Warning("File", file.Name(), "skipped.")
continue
}
r, err := LoadRepository(content)
if err != nil {
ctx.Warning("On parse file", file.Name(), ":", err.Error())
ctx.Warning("File", file.Name(), "skipped.")
continue
}
if r.Name == "" || len(r.Urls) == 0 || r.Type == "" {
ctx.Warning("Invalid repository ", file.Name())
ctx.Warning("File", file.Name(), "skipped.")
continue
}
@@ -359,28 +329,20 @@ func (c *LuetConfig) GetSystemRepository(name string) (*LuetRepository, error) {
return ans, nil
}
func (c *LuetConfig) LoadConfigProtect(ctx *Context) error {
func (c *LuetConfig) loadConfigProtect() error {
var regexConfs = regexp.MustCompile(`.yml$`)
var err error
rootfs := ""
// Respect the rootfs param on read repositories
if !c.ConfigFromHost {
rootfs, err = c.GetSystem().GetRootFsAbs()
if err != nil {
return err
}
rootfs = c.System.Rootfs
}
for _, cdir := range c.ConfigProtectConfDir {
cdir = filepath.Join(rootfs, cdir)
ctx.Debug("Parsing Config Protect Directory", cdir, "...")
files, err := ioutil.ReadDir(cdir)
if err != nil {
ctx.Debug("Skip dir", cdir, ":", err.Error())
continue
}
@@ -390,38 +352,31 @@ func (c *LuetConfig) LoadConfigProtect(ctx *Context) error {
}
if !regexConfs.MatchString(file.Name()) {
ctx.Debug("File", file.Name(), "skipped.")
continue
}
content, err := ioutil.ReadFile(path.Join(cdir, file.Name()))
if err != nil {
ctx.Warning("On read file", file.Name(), ":", err.Error())
ctx.Warning("File", file.Name(), "skipped.")
continue
}
r, err := loadConfigProtectConFile(file.Name(), content)
r, err := loadConfigProtectConfFile(file.Name(), content)
if err != nil {
ctx.Warning("On parse file", file.Name(), ":", err.Error())
ctx.Warning("File", file.Name(), "skipped.")
continue
}
if r.Name == "" || len(r.Directories) == 0 {
ctx.Warning("Invalid config protect file", file.Name())
ctx.Warning("File", file.Name(), "skipped.")
continue
}
c.AddConfigProtectConfFile(r)
c.addProtectFile(r)
}
}
return nil
}
func loadConfigProtectConFile(filename string, data []byte) (*config.ConfigProtectConfFile, error) {
func loadConfigProtectConfFile(filename string, data []byte) (*config.ConfigProtectConfFile, error) {
ans := config.NewConfigProtectConfFile(filename)
err := yaml.Unmarshal(data, &ans)
if err != nil {
@@ -429,47 +384,3 @@ func loadConfigProtectConFile(filename string, data []byte) (*config.ConfigProte
}
return ans, nil
}
func (c *LuetLoggingConfig) SetLogLevel(s LogLevel) {
c.Level = s
}
func (c *LuetSystemConfig) InitTmpDir() error {
if !filepath.IsAbs(c.TmpDirBase) {
abs, err := fileHelper.Rel2Abs(c.TmpDirBase)
if err != nil {
return errors.Wrap(err, "while converting relative path to absolute path")
}
c.TmpDirBase = abs
}
if _, err := os.Stat(c.TmpDirBase); err != nil {
if os.IsNotExist(err) {
err = os.MkdirAll(c.TmpDirBase, os.ModePerm)
if err != nil {
return err
}
}
}
return nil
}
func (c *LuetSystemConfig) CleanupTmpDir() error {
return os.RemoveAll(c.TmpDirBase)
}
func (c *LuetSystemConfig) TempDir(pattern string) (string, error) {
err := c.InitTmpDir()
if err != nil {
return "", err
}
return ioutil.TempDir(c.TmpDirBase, pattern)
}
func (c *LuetSystemConfig) TempFile(pattern string) (*os.File, error) {
err := c.InitTmpDir()
if err != nil {
return nil, err
}
return ioutil.TempFile(c.TmpDirBase, pattern)
}

View File

@@ -17,53 +17,86 @@
package types_test
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
types "github.com/mudler/luet/pkg/api/core/types"
"github.com/mudler/luet/pkg/api/core/context"
"github.com/mudler/luet/pkg/api/core/types"
fileHelper "github.com/mudler/luet/pkg/helpers/file"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Config", func() {
Context("Load Repository1", func() {
ctx := types.NewContext()
ctx.Config.RepositoriesConfDir = []string{
"../../../../tests/fixtures/repos.conf.d",
}
err := ctx.Config.LoadRepositories(ctx)
Context("Inits paths", func() {
t, _ := ioutil.TempDir("", "tests")
defer os.RemoveAll(t)
c := &types.LuetConfig{
System: types.LuetSystemConfig{
Rootfs: t,
PkgsCachePath: "foo",
DatabasePath: "baz",
}}
It("sets default", func() {
err := c.Init()
Expect(err).ToNot(HaveOccurred())
Expect(c.System.Rootfs).To(Equal(t))
Expect(c.System.PkgsCachePath).To(Equal(filepath.Join(t, "baz", "foo")))
Expect(c.System.DatabasePath).To(Equal(filepath.Join(t, "baz")))
})
})
Context("Load Repository1", func() {
var ctx *context.Context
BeforeEach(func() {
ctx = context.NewContext(context.WithConfig(&types.LuetConfig{
RepositoriesConfDir: []string{
"../../../../tests/fixtures/repos.conf.d",
},
}))
ctx.Config.Init()
})
It("Check Load Repository 1", func() {
Expect(err).Should(BeNil())
Expect(len(ctx.Config.SystemRepositories)).Should(Equal(2))
Expect(ctx.Config.SystemRepositories[0].Name).Should(Equal("test1"))
Expect(ctx.Config.SystemRepositories[0].Priority).Should(Equal(999))
Expect(ctx.Config.SystemRepositories[0].Type).Should(Equal("disk"))
Expect(len(ctx.Config.SystemRepositories[0].Urls)).Should(Equal(1))
Expect(ctx.Config.SystemRepositories[0].Urls[0]).Should(Equal("tests/repos/test1"))
Expect(len(ctx.GetConfig().SystemRepositories)).Should(Equal(2))
Expect(ctx.GetConfig().SystemRepositories[0].Name).Should(Equal("test1"))
Expect(ctx.GetConfig().SystemRepositories[0].Priority).Should(Equal(999))
Expect(ctx.GetConfig().SystemRepositories[0].Type).Should(Equal("disk"))
Expect(len(ctx.GetConfig().SystemRepositories[0].Urls)).Should(Equal(1))
Expect(ctx.GetConfig().SystemRepositories[0].Urls[0]).Should(Equal("tests/repos/test1"))
})
It("Chec Load Repository 2", func() {
Expect(err).Should(BeNil())
Expect(len(ctx.Config.SystemRepositories)).Should(Equal(2))
Expect(ctx.Config.SystemRepositories[1].Name).Should(Equal("test2"))
Expect(ctx.Config.SystemRepositories[1].Priority).Should(Equal(1000))
Expect(ctx.Config.SystemRepositories[1].Type).Should(Equal("disk"))
Expect(len(ctx.Config.SystemRepositories[1].Urls)).Should(Equal(1))
Expect(ctx.Config.SystemRepositories[1].Urls[0]).Should(Equal("tests/repos/test2"))
Expect(len(ctx.GetConfig().SystemRepositories)).Should(Equal(2))
Expect(ctx.GetConfig().SystemRepositories[1].Name).Should(Equal("test2"))
Expect(ctx.GetConfig().SystemRepositories[1].Priority).Should(Equal(1000))
Expect(ctx.GetConfig().SystemRepositories[1].Type).Should(Equal("disk"))
Expect(len(ctx.GetConfig().SystemRepositories[1].Urls)).Should(Equal(1))
Expect(ctx.GetConfig().SystemRepositories[1].Urls[0]).Should(Equal("tests/repos/test2"))
})
})
Context("Simple temporary directory creation", func() {
ctx := context.NewContext(context.WithConfig(&types.LuetConfig{
System: types.LuetSystemConfig{
TmpDirBase: os.TempDir() + "/tmpluet",
},
}))
BeforeEach(func() {
ctx = context.NewContext(context.WithConfig(&types.LuetConfig{
System: types.LuetSystemConfig{
TmpDirBase: os.TempDir() + "/tmpluet",
},
}))
})
It("Create Temporary directory", func() {
ctx := types.NewContext()
ctx.Config.GetSystem().TmpDirBase = os.TempDir() + "/tmpluet"
tmpDir, err := ctx.Config.GetSystem().TempDir("test1")
tmpDir, err := ctx.TempDir("test1")
Expect(err).ToNot(HaveOccurred())
Expect(strings.HasPrefix(tmpDir, filepath.Join(os.TempDir(), "tmpluet"))).To(BeTrue())
Expect(fileHelper.Exists(tmpDir)).To(BeTrue())
@@ -72,11 +105,7 @@ var _ = Describe("Config", func() {
})
It("Create Temporary file", func() {
ctx := types.NewContext()
ctx.Config.GetSystem().TmpDirBase = os.TempDir() + "/tmpluet"
tmpFile, err := ctx.Config.GetSystem().TempFile("testfile1")
tmpFile, err := ctx.TempFile("testfile1")
Expect(err).ToNot(HaveOccurred())
Expect(strings.HasPrefix(tmpFile.Name(), filepath.Join(os.TempDir(), "tmpluet"))).To(BeTrue())
Expect(fileHelper.Exists(tmpFile.Name())).To(BeTrue())

View File

@@ -15,417 +15,16 @@
package types
import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
type Context interface {
Logger
GarbageCollector
GetConfig() LuetConfig
Copy() Context
// SetAnnotation sets generic annotations to hold in a context
SetAnnotation(s string, i interface{})
"github.com/kyokomi/emoji"
"github.com/mudler/luet/pkg/helpers/terminal"
"github.com/pkg/errors"
"github.com/pterm/pterm"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/term"
)
// GetAnnotation gets generic annotations to hold in a context
GetAnnotation(s string) interface{}
const (
ErrorLevel LogLevel = "error"
WarningLevel LogLevel = "warning"
InfoLevel LogLevel = "info"
SuccessLevel LogLevel = "success"
FatalLevel LogLevel = "fatal"
)
type Context struct {
context.Context
Config *LuetConfig
IsTerminal bool
NoSpinner bool
name string
s *pterm.SpinnerPrinter
spinnerLock *sync.Mutex
z *zap.Logger
ProgressBar *pterm.ProgressbarPrinter
}
func NewContext() *Context {
return &Context{
spinnerLock: &sync.Mutex{},
IsTerminal: terminal.IsTerminal(os.Stdout),
Config: &LuetConfig{
ConfigFromHost: true,
Logging: LuetLoggingConfig{},
General: LuetGeneralConfig{},
System: LuetSystemConfig{
DatabasePath: filepath.Join("var", "db", "packages"),
TmpDirBase: filepath.Join(os.TempDir(), "tmpluet")},
Solver: LuetSolverOptions{},
},
s: pterm.DefaultSpinner.WithShowTimer(false).WithRemoveWhenDone(true),
}
}
func (c *Context) WithName(name string) *Context {
newc := c.Copy()
newc.name = name
return newc
}
func (c *Context) Copy() *Context {
configCopy := *c.Config
configCopy.System = *c.Config.GetSystem()
configCopy.General = *c.Config.GetGeneral()
configCopy.Logging = *c.Config.GetLogging()
ctx := *c
ctxCopy := &ctx
ctxCopy.Config = &configCopy
return ctxCopy
}
// GetTerminalSize returns the width and the height of the active terminal.
func (c *Context) GetTerminalSize() (width, height int, err error) {
w, h, err := term.GetSize(int(os.Stdout.Fd()))
if w <= 0 {
w = 0
}
if h <= 0 {
h = 0
}
if err != nil {
err = errors.New("size not detectable")
}
return w, h, err
}
func (c *Context) Init() (err error) {
if c.IsTerminal {
if !c.Config.Logging.Color {
c.Debug("Disabling colors")
c.NoColor()
}
} else {
c.Debug("Not a terminal, disabling colors")
c.NoColor()
}
if c.Config.General.Quiet {
c.NoColor()
pterm.DisableStyling()
}
c.Debug("Colors", c.Config.GetLogging().Color)
c.Debug("Logging level", c.Config.GetLogging().Level)
c.Debug("Debug mode", c.Config.GetGeneral().Debug)
if c.Config.GetLogging().EnableLogFile && c.Config.GetLogging().Path != "" {
// Init zap logger
err = c.InitZap()
if err != nil {
return
}
}
// Load repositories
err = c.Config.LoadRepositories(c)
if err != nil {
return
}
return
}
func (c *Context) NoColor() {
pterm.DisableColor()
}
func (c *Context) Ask() bool {
var input string
c.Info("Do you want to continue with this operation? [y/N]: ")
_, err := fmt.Scanln(&input)
if err != nil {
return false
}
input = strings.ToLower(input)
if input == "y" || input == "yes" {
return true
}
return false
}
func (c *Context) InitZap() error {
var err error
if c.z == nil {
// TODO: test permission for open logfile.
cfg := zap.NewProductionConfig()
cfg.OutputPaths = []string{c.Config.GetLogging().Path}
cfg.Level = c.Config.GetLogging().Level.ZapLevel()
cfg.ErrorOutputPaths = []string{}
if c.Config.GetLogging().JsonFormat {
cfg.Encoding = "json"
} else {
cfg.Encoding = "console"
}
cfg.DisableCaller = true
cfg.DisableStacktrace = true
cfg.EncoderConfig.TimeKey = "time"
cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
c.z, err = cfg.Build()
if err != nil {
fmt.Fprint(os.Stderr, "Error on initialize file logger: "+err.Error()+"\n")
return err
}
}
return nil
}
// Spinner starts the spinner
func (c *Context) Spinner() {
if !c.IsTerminal || c.NoSpinner {
return
}
c.spinnerLock.Lock()
defer c.spinnerLock.Unlock()
var confLevel int
if c.Config.GetGeneral().Debug {
confLevel = 3
} else {
confLevel = c.Config.GetLogging().Level.ToNumber()
}
if 2 > confLevel {
return
}
if !c.s.IsActive {
c.s, _ = c.s.Start()
}
}
func (c *Context) Screen(text string) {
pterm.DefaultHeader.WithBackgroundStyle(pterm.NewStyle(pterm.BgLightBlue)).WithMargin(2).Println(text)
//pterm.DefaultCenter.Print(pterm.DefaultHeader.WithFullWidth().WithBackgroundStyle(pterm.NewStyle(pterm.BgLightBlue)).WithMargin(10).Sprint(text))
}
func (c *Context) SpinnerText(suffix, prefix string) {
if !c.IsTerminal || c.NoSpinner {
return
}
c.spinnerLock.Lock()
defer c.spinnerLock.Unlock()
if c.Config.GetGeneral().Debug {
fmt.Printf("%s %s\n",
suffix, prefix,
)
} else {
c.s.UpdateText(suffix + prefix)
}
}
func (c *Context) SpinnerStop() {
if !c.IsTerminal {
return
}
c.spinnerLock.Lock()
defer c.spinnerLock.Unlock()
var confLevel int
if c.Config.GetGeneral().Debug {
confLevel = 3
} else {
confLevel = c.Config.GetLogging().Level.ToNumber()
}
if 2 > confLevel {
return
}
if c.s != nil {
c.s.Success()
}
}
func (c *Context) log2File(level LogLevel, msg string) {
switch level {
case FatalLevel:
c.z.Fatal(msg)
case ErrorLevel:
c.z.Error(msg)
case WarningLevel:
c.z.Warn(msg)
case InfoLevel, SuccessLevel:
c.z.Info(msg)
default:
c.z.Debug(msg)
}
}
func (c *Context) Msg(level LogLevel, ln bool, msg ...interface{}) {
var message string
var confLevel, msgLevel int
if c.Config.GetGeneral().Debug {
confLevel = 3
pterm.EnableDebugMessages()
} else {
confLevel = c.Config.GetLogging().Level.ToNumber()
}
msgLevel = level.ToNumber()
if msgLevel > confLevel {
return
}
for _, m := range msg {
message += " " + fmt.Sprintf("%v", m)
}
// Color message
levelMsg := message
if c.Config.GetLogging().Color {
switch level {
case WarningLevel:
levelMsg = pterm.LightYellow(":construction: warning" + message)
case InfoLevel:
levelMsg = message
case SuccessLevel:
levelMsg = pterm.LightGreen(message)
case ErrorLevel:
levelMsg = pterm.Red(message)
default:
levelMsg = pterm.Blue(message)
}
}
// Strip emoji if needed
if c.Config.GetLogging().EnableEmoji && c.IsTerminal {
levelMsg = emoji.Sprint(levelMsg)
} else {
re := regexp.MustCompile(`[:][\w]+[:]`)
levelMsg = re.ReplaceAllString(levelMsg, "")
}
if c.name != "" {
levelMsg = fmt.Sprintf("[%s] %s", c.name, levelMsg)
}
if c.z != nil {
c.log2File(level, message)
}
// Print the message based on the level
switch level {
case SuccessLevel:
if ln {
pterm.Success.Println(levelMsg)
} else {
pterm.Success.Print(levelMsg)
}
case InfoLevel:
if ln {
pterm.Info.Println(levelMsg)
} else {
pterm.Info.Print(levelMsg)
}
case WarningLevel:
if ln {
pterm.Warning.Println(levelMsg)
} else {
pterm.Warning.Print(levelMsg)
}
case ErrorLevel:
if ln {
pterm.Error.Println(levelMsg)
} else {
pterm.Error.Print(levelMsg)
}
case FatalLevel:
if ln {
pterm.Fatal.Println(levelMsg)
} else {
pterm.Fatal.Print(levelMsg)
}
default:
if ln {
pterm.Debug.Println(levelMsg)
} else {
pterm.Debug.Print(levelMsg)
}
}
}
func (c *Context) Warning(mess ...interface{}) {
c.Msg("warning", true, mess...)
if c.Config.GetGeneral().FatalWarns {
os.Exit(2)
}
}
func (c *Context) Debug(mess ...interface{}) {
pc, file, line, ok := runtime.Caller(1)
if ok {
mess = append([]interface{}{fmt.Sprintf("(%s:#%d:%v)",
path.Base(file), line, runtime.FuncForPC(pc).Name())}, mess...)
}
c.Msg("debug", true, mess...)
}
func (c *Context) Info(mess ...interface{}) {
c.Msg("info", true, mess...)
}
func (c *Context) Success(mess ...interface{}) {
c.Msg("success", true, mess...)
}
func (c *Context) Error(mess ...interface{}) {
c.Msg("error", true, mess...)
}
func (c *Context) Fatal(mess ...interface{}) {
c.Error(mess...)
os.Exit(1)
}
type LogLevel string
func (level LogLevel) ToNumber() int {
switch level {
case ErrorLevel, FatalLevel:
return 0
case WarningLevel:
return 1
case InfoLevel, SuccessLevel:
return 2
default: // debug
return 3
}
}
func (level LogLevel) ZapLevel() zap.AtomicLevel {
switch level {
case FatalLevel:
return zap.NewAtomicLevelAt(zap.FatalLevel)
case ErrorLevel:
return zap.NewAtomicLevelAt(zap.ErrorLevel)
case WarningLevel:
return zap.NewAtomicLevelAt(zap.WarnLevel)
case InfoLevel, SuccessLevel:
return zap.NewAtomicLevelAt(zap.InfoLevel)
default:
return zap.NewAtomicLevelAt(zap.DebugLevel)
}
WithLoggingContext(s string) Context
}

View File

@@ -0,0 +1,25 @@
// Copyright © 2021 Ettore Di Giacinto <mudler@mocaccino.org>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see <http://www.gnu.org/licenses/>.
package types
import "os"
type GarbageCollector interface {
Clean() error
TempDir(pattern string) (string, error)
TempFile(s string) (*os.File, error)
String() string
}

View File

@@ -0,0 +1,41 @@
// Copyright © 2021 Ettore Di Giacinto <mudler@mocaccino.org>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see <http://www.gnu.org/licenses/>.
package types
// Logger is a standard logging interface
type Logger interface {
Info(...interface{})
Success(...interface{})
Warning(...interface{})
Warn(...interface{})
Debug(...interface{})
Error(...interface{})
Fatal(...interface{})
Panic(...interface{})
Trace(...interface{})
Infof(string, ...interface{})
Warnf(string, ...interface{})
Debugf(string, ...interface{})
Errorf(string, ...interface{})
Fatalf(string, ...interface{})
Panicf(string, ...interface{})
Tracef(string, ...interface{})
SpinnerStop()
Spinner()
Ask() bool
Screen(string)
}