From 486281525af5d0df2ab65a465736c9513fa964f4 Mon Sep 17 00:00:00 2001 From: Matthew Rich Date: Tue, 24 Sep 2024 19:26:40 +0000 Subject: [PATCH] add client pkg --- internal/client/client.go | 305 +++++++++++++++++++++++++++++++++ internal/client/client_test.go | 211 +++++++++++++++++++++++ 2 files changed, 516 insertions(+) create mode 100644 internal/client/client.go create mode 100644 internal/client/client_test.go diff --git a/internal/client/client.go b/internal/client/client.go new file mode 100644 index 0000000..174cf09 --- /dev/null +++ b/internal/client/client.go @@ -0,0 +1,305 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package client + +import ( + "decl/internal/data" + "decl/internal/folio" +_ "decl/internal/fan" +_ "decl/internal/config" +_ "decl/internal/resource" + "decl/internal/fs" +_ "errors" + "fmt" + "context" + "log/slog" + "os" +) + + +var ( +) + +type App struct { + Target folio.URI + ImportedMap map[folio.URI]data.Document + Documents []data.Document + emitter data.Converter + merged data.Document + Config data.Document +} + +func NewClient() *App { + a := &App{ ImportedMap: make(map[folio.URI]data.Document), Documents: make([]data.Document, 0, 100) } + return a +} + +// Load config documents from default system config path. Ignore if missing. +func (a *App) SystemConfiguration(configPath string) (err error) { + var extractor data.Converter + var sourceResource data.Resource + if a.Config == nil { + a.Config = folio.DocumentRegistry.NewDocument("file:///etc/jx/runtimeconfig.jx.yaml") + } + if configPath != "" { + //configURI := folio.URI(configPath) + var loaded []data.Document + docFs := fs.NewWalkDir(os.DirFS(configPath), configPath, func(fsys fs.FS, path string, file fs.DirEntry) (loadErr error) { + u := folio.URI(fmt.Sprintf("file://%s", path)) + + if ! file.IsDir() { + if extractor, loadErr = folio.DocumentRegistry.ConverterTypes.New(string(u)); loadErr == nil { + if sourceResource, loadErr = u.NewResource(nil); loadErr == nil { + if loaded, loadErr = extractor.(data.ManyExtractor).ExtractMany(sourceResource, nil); loadErr == nil { + a.Config.AppendConfigurations(loaded) + } + } + } + } + return + }) + err = docFs.Walk(nil) + } + return +} + +func (a *App) MergeDocuments() { + a.merged = folio.DocumentRegistry.NewDocument("file://-") + for _, d := range a.Documents { + for _, declaration := range d.(*folio.Document).ResourceDeclarations { + a.merged.AddDeclaration((data.Declaration)(declaration)) + } + } +} + +func (a *App) SetOutput(uri string) (err error) { + if uri == "-" { + uri = "jx://-" + } + a.Target = folio.URI(uri) + if a.emitter, err = folio.DocumentRegistry.ConverterTypes.New(uri); err != nil { + return fmt.Errorf("Failed opening target: %s, %w", uri, err) + } + return +} + +// Each document has an `imports` keyword which can be used to load dependencies +func (a *App) LoadDocumentImports() error { + for _, d := range a.Documents { + for _, importedDocument := range d.ImportedDocuments() { + docURI := folio.URI(importedDocument.GetURI()) + if _, ok := a.ImportedMap[docURI]; !ok { + a.ImportedMap[docURI] = importedDocument + if _, outputErr := a.emitter.Emit(importedDocument, nil); outputErr != nil { + return outputErr + } + } + } + } + return nil +} + + +func (a *App) ImportResource(ctx context.Context, uri string) (err error) { + if len(a.Documents) < 1 { + a.Documents = append(a.Documents, folio.DocumentRegistry.NewDocument("")) + } + resourceURI := folio.URI(uri) + u := resourceURI.Parse() + if u == nil { + return fmt.Errorf("Failed adding resource: %s", uri) + } + if u.Scheme == "" { + u.Scheme = "file" + } + + for _, d := range a.Documents { + if newResource, newResourceErr := d.NewResource(uri); newResourceErr == nil { + if _, err = newResource.Read(ctx); err != nil { + return + } + } else { + return newResourceErr + } + } + return +} + +func (a *App) ImportSource(uri string) (loadedDocuments []data.Document, err error) { + if loadedDocuments, err = folio.DocumentRegistry.Load(folio.URI(uri)); err == nil && loadedDocuments != nil { + a.Documents = append(a.Documents, loadedDocuments...) + } else { + return + } + return +} + +func (a *App) Import(docs []string) (err error) { + for _, source := range docs { + if _, err = a.ImportSource(source); err != nil { + return + } + } + return +} + +func (a *App) Apply(ctx context.Context, deleteResources bool) (err error) { + for _, d := range a.Documents { + d.SetConfig(a.Config) + + var overrideState string = "" + if deleteResources { + overrideState = "delete" + } + d.ResolveIds(ctx) + + if ! d.CheckConstraints() { + slog.Info("Client.Apply() document constrains failed", "requires", d) + continue + } + + if e := d.(*folio.Document).Apply(overrideState); e != nil { + slog.Info("Client.Apply() error", "error", e) + return e + } + if d.Failures() > 0 { + err = fmt.Errorf("Failed resources: %d, %w", d.Failures(), err) + } + } + return +} + +func (a *App) ImportCmd(ctx context.Context, docs []string, resourceURI string, quiet bool, merge bool) (err error) { + if err = a.Import(docs); err != nil { + return + } + + if err = a.LoadDocumentImports(); err != nil { + return + } + + if len(resourceURI) > 0 { + if err = a.ImportResource(ctx, resourceURI); err != nil { + return + } + } + + if quiet { + err = a.Quiet() + } else { + if merge { + a.MergeDocuments() + } + err = a.Emit() + if err != nil { + return + } + } + return +} + +func (a *App) ApplyCmd(ctx context.Context, docs []string, quiet bool, deleteResources bool) (err error) { + if err = a.Import(docs); err != nil { + return + } + + if err = a.LoadDocumentImports(); err != nil { + return + } + + if err = a.Apply(ctx, deleteResources); err != nil { + return + } + + if quiet { + err = a.Quiet() + } else { + err = a.Emit() + if err != nil { + return + } + } + return +} + +func (a *App) Diff(left []data.Document, right []data.Document) (err error) { + output := os.Stdout + slog.Info("jx diff ", "right", right, "left", left) + index := 0 + for { + if index >= len(right) && index >= len(left) { + break + } + if index >= len(right) { + if _, err = left[index].Diff(folio.DocumentRegistry.NewDocument(""), output); err != nil { + return + } + index++ + continue + } + if index >= len(left) { + if _, err = folio.DocumentRegistry.NewDocument("").Diff(right[index], output); err != nil { + return + } + index++ + continue + } + if _, err = left[index].Diff(right[index], output); err != nil { + return + } + index++ + } + return +} + +func (a *App) DiffCmd(docs []string) (err error) { + output := os.Stdout + + var leftDocuments, rightDocuments []data.Document + + //leftSource := folio.URI(docs[0]) + rightSource := folio.URI(docs[1]) + + if leftDocuments, err = a.ImportSource(docs[0]); err == nil { + if rightSource.IsEmpty() { + for _, doc := range leftDocuments { + _, err = doc.DiffState(output) + } + } else { + rightDocuments, err = a.ImportSource(docs[1]) + err = a.Diff(leftDocuments, rightDocuments) + } + } + return err +} + +func (a *App) Quiet() (err error) { + output := os.Stdout + for _, d := range a.Documents { + for _, dr := range d.Declarations() { + if _, err = output.Write([]byte(fmt.Sprintf("%s\n", dr.Resource().URI()))); err != nil { + return + } + } + } + return +} + +func (a *App) Emit() (err error) { + if a.merged == nil { + for _, d := range a.Documents { + if _, err = a.emitter.Emit(d, nil); err != nil { + return + } + } + } else { + if _, err = a.emitter.Emit(a.merged, nil); err != nil { + return + } + } + return +} + +func (a *App) Close() (err error) { + return a.emitter.Close() +} diff --git a/internal/client/client_test.go b/internal/client/client_test.go new file mode 100644 index 0000000..884da13 --- /dev/null +++ b/internal/client/client_test.go @@ -0,0 +1,211 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package client + +import ( + "github.com/stretchr/testify/assert" + "os" + "os/user" + "os/exec" + "testing" + "decl/internal/tempdir" + "log" + "decl/internal/folio" +_ "decl/internal/fan" + "decl/internal/codec" + "decl/internal/data" + "context" + "fmt" + "log/slog" +) + +var programLevel = new(slog.LevelVar) + +var TempDir tempdir.Path = "jx_client" + +var ProcessTestUserName string +var ProcessTestGroupName string + +func TestMain(m *testing.M) { + LoggerConfig() + var err error + err = TempDir.Create() + if err != nil || TempDir == "" { + log.Fatal(err) + } + + ProcessTestUserName, ProcessTestGroupName = ProcessUserName() + rc := m.Run() + + TempDir.Remove() + os.Exit(rc) +} + +func LoggerConfig() { + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel})) + slog.SetDefault(logger) + programLevel.Set(slog.LevelDebug) +} + +func ProcessUserName() (string, string) { + processUser, userErr := user.Current() + if userErr != nil { + panic(userErr) + } + processGroup, groupErr := user.LookupGroupId(processUser.Gid) + if groupErr != nil { + panic(groupErr) + } + return processUser.Username, processGroup.Name +} + +func ExitError(e error) string { + if e != nil { + switch v := e.(type) { + case *exec.ExitError: + return string(v.Stderr) + default: + return e.Error() + } + } + return "" +} + +// jx import ... +func TestClientImport(t *testing.T) { + c := NewClient() + assert.NotNil(t, c) + + importDocuments := []string{ + "file://../../examples/file.jx.yaml", + "file://../../examples/user.jx.yaml", + } + + assert.Nil(t, c.Import(importDocuments)) + + for index, uri := range importDocuments { + u := folio.URI(uri) + r, readerErr := u.ContentReaderStream() + assert.Nil(t, readerErr) + assert.NotNil(t, r) + + doc := folio.DocumentRegistry.NewDocument(folio.URI(uri)) + assert.Nil(t, doc.LoadReader(r, codec.FormatYaml)) + + imported := c.Documents[index] + assert.NotNil(t, imported) + assert.Equal(t, uri, imported.GetURI()) + assert.Equal(t, doc.Len(), imported.Len()) + } +} + +// jx import --resource +func TestClientImportResource(t *testing.T) { + ctx := context.Background() + c := NewClient() + assert.NotNil(t, c) + + importResources := []string{ + "file://../../COPYRIGHT", + } + + for _, uri := range importResources { + assert.Nil(t, c.ImportResource(ctx, uri)) + } + + imported := c.Documents[0] + assert.NotNil(t, imported) + for _, uri := range importResources { + assert.NotNil(t, imported.(*folio.Document).GetResource(uri)) + } +} + +func TestClientEmit(t *testing.T) { + //ctx := context.Background() + c := NewClient() + assert.NotNil(t, c) + + importDocuments := []string{ + "file://../../examples/file.jx.yaml", + "file://../../examples/user.jx.yaml", + } + + assert.Nil(t, c.Import(importDocuments)) + targetFile := TempDir.FilePath("jx_emit_output.jx.yaml") + targetFileURI := fmt.Sprintf("file://%s", targetFile) + assert.Nil(t, c.SetOutput(targetFile)) + assert.Nil(t, c.Emit()) + + assert.FileExists(t, targetFile) + + + u := folio.URI(targetFileURI) + r, readerErr := u.ContentReaderStream() + assert.Nil(t, readerErr) + assert.NotNil(t, r) + + extractor, err := folio.DocumentRegistry.ConverterTypes.New(targetFileURI) + assert.Nil(t, err) + assert.NotNil(t, extractor) + + targetResource, resErr := u.NewResource(nil) + assert.Nil(t, resErr) + docs, exErr := extractor.(data.ManyExtractor).ExtractMany(targetResource, nil) + assert.Nil(t, exErr) + + assert.Equal(t, 2, len(docs)) + + assert.Equal(t, 1, docs[1].Len()) +} + +func BenchmarkClientSystemConfigurations(b *testing.B) { + assert.Nil(b, TempDir.Mkdir("benchconfig", 0700)) + ConfDir := tempdir.Path(TempDir.FilePath("benchconfig")) + ConfDir.CreateFile("cfg.jx.yaml", ` +configurations: +- name: files + values: + prefix: /usr +`) + + configDirURI := fmt.Sprintf("file://%s", ConfDir) + + programLevel.Set(slog.LevelError) + b.Run("systemconfiguration", func(b *testing.B) { + for i := 0; i < b.N; i++ { + c := NewClient() + c.SystemConfiguration(configDirURI) + } + }) + programLevel.Set(slog.LevelDebug) +} + +func TestClientSystemConfiguration(t *testing.T) { + c := NewClient() + assert.NotNil(t, c) + + assert.Nil(t, TempDir.Mkdir("config", 0700)) + + ConfDir := tempdir.Path(TempDir.FilePath("config")) + ConfDir.CreateFile("cfg.jx.yaml", ` +configurations: +- name: files + values: + prefix: /usr +`) + + //configDirURI := fmt.Sprintf("file://%s", ConfDir) + configErr := c.SystemConfiguration(string(ConfDir)) + assert.Nil(t, configErr) + + assert.NotNil(t, c.Config) + + slog.Info("TestClientSystemConfiguration", "config", c.Config) + cfg := c.Config.GetConfig("files") + assert.NotNil(t, cfg) + + value, valueErr := cfg.GetValue("prefix") + assert.Nil(t, valueErr) + assert.Equal(t, "/usr", value.(string)) + return +}