Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
*.gen.go
.launchr/
.compose/
.plasmactl
80 changes: 46 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## Composition Tool Specification

The composition tool is a command-line tool that helps developers manage
dependencies for their projects. It allows developers to specify the dependencies for
a project in a "plasma-compose.yaml" file, and then fetches and installs those dependencies
Expand All @@ -8,28 +9,39 @@ The tool works by recursively fetching and processing the "plasma-compose.yaml"
and its dependencies, and then merging the resulting filesystems into a single filesystem.

### CLI

The composition tool is invoked from the command line with the following syntax:
launchr compose [options]
Where options are:

* -w, --working-dir : The directory where temporary files should be stored during the
composition process. Default is the .compose/packages
* -s, --skip-not-versioned : Skip not versioned files from source directory (git only)
* --conflicts-verbosity: Log files conflicts in format "[curent-package] - path to file > Selectef from [domain, other package or current-package]"
* --conflicts-verbosity: Log files conflicts in format "[current-package] - path to file > Selected
from [domain, other package or current-package]"
* --interactive: Interactive mode allows to submit user credentials during action (default: true)

Example usage - `launchr compose -w=./folder/something -s=1 or -s=true --conflicts-verbosity`

It's important to note that: if same file is present locally and also brought by a package, default strategy is that local file will be taken and package file ignored. [Different strategies](https://github.com/launchrctl/compose/blob/main/example/compose.example.yaml#L18-L35) can be difined to customize this behavior to your needs.

It's important to note that: if same file is present locally and also brought by a package, default strategy is that
local file will be taken and package file
ignored. [Different strategies](https://github.com/launchrctl/compose/blob/main/example/compose.example.yaml#L18-L35)
can be difined to customize this behavior to your needs.

### `plasma-compose.yaml` File Format
The "plasma-compose.yaml" file is a text file that specifies the dependencies for a package, along with any necessary metadata and sources for those dependencies.

The "plasma-compose.yaml" file is a text file that specifies the dependencies for a package, along with any necessary
metadata and sources for those dependencies.
The file format includes the following elements:

- name: The name of the package.
- version: The version number of the package.
- source: The source for the package, including the type of source (Git, HTTP), URL or file path, merge strategy and other metadata.
- source: The source for the package, including the type of source (Git, HTTP), URL or file path, merge strategy and
other metadata.
- dependencies: A list of required dependencies.

List of strategies:

- overwrite-local-file
- remove-extra-local-files
- ignore-extra-package-files
Expand All @@ -38,40 +50,43 @@ List of strategies:
Example:

```yaml
name: myproject
version: 1.0.0
name: example
dependencies:
- name: compose-example
source:
type: git
ref: master
tag: 0.0.1
url: https://github.com/example/compose-example.git
strategy:
- name: remove-extra-local-files
path:
- path/to/remove-extra-local-files
- name: ignore-extra-package-files
path:
- library/inventories/platform_nodes/configuration/dev.yaml
- library/inventories/platform_nodes/configuration/prod.yaml
- library/inventories/platform_nodes/configuration/whatever.yaml
- name: compose-example
source:
type: git
ref: master # branch or tag name
url: https://github.com/example/compose-example.git
strategy:
- name: remove-extra-local-files
path:
- path/to/remove-extra-local-files
- name: ignore-extra-package-files
path:
- library/inventories/platform_nodes/configuration/dev.yaml
- library/inventories/platform_nodes/configuration/prod.yaml
- library/inventories/platform_nodes/configuration/whatever.yaml
```


### Fetching and Installing Dependencies
The composition tool fetches and installs dependencies for a package by recursively processing the "plasma-compose.yaml" files for each package and its dependencies. The tool follows these general steps:

1. Fetch the package source code from the specified source location.
2. Extract the package contents to a temporary directory.
3. Process the "plasma-compose.yaml" file for the package, fetching and installing any necessary dependencies recursively.
4. Merge the package filesystem into the final platform filesystem.
5. Repeat steps 1-4 for each package and its dependencies.
The composition tool fetches and installs dependencies for a package by recursively processing the "plasma-compose.yaml"
files for each package and its dependencies. The tool follows these general steps:

1. Check if package exists locally and is up-to-date. If it's not, remove it from packages dir and proceed to next step.
2. Fetch the package from the specified location.
3. Extract the package contents to a packages directory.
4. Process the "plasma-compose.yaml" file for the package, fetching and installing any necessary dependencies
recursively.
5. Merge the package filesystem into the final platform filesystem.
6. Repeat steps 1-5 for each package and its dependencies.

During this process, the composition tool keeps track of the dependencies for each package.

### Plasma-compose commands

it's possible to manipulate plasma-compose.yaml file using commands:

- plasmactl compose:add
- plasmactl compose:update
- plasmactl compose:delete
Expand All @@ -84,17 +99,14 @@ For `compose:delete` it's possible to pass list of packaged to delete.

In other cases, user will be prompted to CLI form to fill necessary data of packages.

Example of usage

Examples of usage

```
launchr compose:add --url some-url --type http
launchr compose:add --package package-name --url some-url --ref v1.0.0
launchr compose:update --package package-name --url some-url --ref v1.0.0

launchr compose:add --package package-name --url some-url --ref v1.0.0 --strategy overwrite-local-file --strategy-path "path1|path2"
launchr compose:add --package package-name --url some-url --ref v1.0.0 --strategy overwrite-local-file,remove-extra-local-files --strategy-path "path1|path2,path3|path4"
launchr compose:add --package package-name --url some-url --ref branch --strategy overwrite-local-file,remove-extra-local-files --strategy-path "path1|path2,path3|path4"
launchr compose:add --package package-name --url some-url --ref v1.0.0 --strategy overwrite-local-file --strategy-path "path1|path2" --strategy remove-extra-local-files --strategy-path "path3|path4"


```
178 changes: 97 additions & 81 deletions compose/builder.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package compose

import (
"context"
"fmt"
"io"
"io/fs"
Expand Down Expand Up @@ -150,7 +151,7 @@ func getVersionedMap(gitDir string) (map[string]bool, error) {
return versionedFiles, err
}

func (b *Builder) build() error {
func (b *Builder) build(ctx context.Context) error {
launchr.Term().Println("Creating composition...")
err := EnsureDirExists(b.targetDir)
if err != nil {
Expand All @@ -174,43 +175,48 @@ func (b *Builder) build() error {

// @todo move to function
err = fs.WalkDir(baseFs, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}

root := rgxPathRoot.FindString(path)
if _, ok := excludedFolders[root]; ok {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
default:
if err != nil {
return err
}

if !d.IsDir() {
filename := filepath.Base(path)
if _, ok := excludedFiles[filename]; ok {
root := rgxPathRoot.FindString(path)
if _, ok := excludedFolders[root]; ok {
return nil
}
}

// Apply strategies that target local files
for _, localStrategy := range ls {
if localStrategy.s == removeExtraLocalFiles {
if ensureStrategyPrefixPath(path, localStrategy.paths) {
if !d.IsDir() {
filename := filepath.Base(path)
if _, ok := excludedFiles[filename]; ok {
return nil
}
}
}

// Add .git folder into entriesTree whenever CheckVersioned or not
if checkVersioned && !strings.HasPrefix(path, gitPrefix) {
if _, ok := versionedMap[path]; !ok {
return nil
// Apply strategies that target local files
for _, localStrategy := range ls {
if localStrategy.s == removeExtraLocalFiles {
if ensureStrategyPrefixPath(path, localStrategy.paths) {
return nil
}
}
}
}

finfo, _ := d.Info()
entry := &fsEntry{Prefix: b.platformDir, Path: path, Entry: finfo, Excluded: false, From: "domain repo"}
entriesTree = append(entriesTree, entry)
entriesMap[path] = entry
return nil
// Add .git folder into entriesTree whenever CheckVersioned or not
if checkVersioned && !strings.HasPrefix(path, gitPrefix) {
if _, ok := versionedMap[path]; !ok {
return nil
}
}

finfo, _ := d.Info()
entry := &fsEntry{Prefix: b.platformDir, Path: path, Entry: finfo, Excluded: false, From: "domain repo"}
entriesTree = append(entriesTree, entry)
entriesMap[path] = entry
return nil
}
})

if err != nil {
Expand All @@ -226,72 +232,82 @@ func (b *Builder) build() error {
}

for i := 0; i < len(items); i++ {
pkgName := items[i]
if pkgName != DependencyRoot {
pkgPath := filepath.Join(b.sourceDir, pkgName, targetsMap[pkgName])
packageFs := os.DirFS(pkgPath)
strategies, ok := ps[pkgName]
err = fs.WalkDir(packageFs, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
default:
pkgName := items[i]
if pkgName != DependencyRoot {
pkgPath := filepath.Join(b.sourceDir, pkgName, targetsMap[pkgName])
packageFs := os.DirFS(pkgPath)
strategies, ok := ps[pkgName]
err = fs.WalkDir(packageFs, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}

// Skip .git folder from packages
if strings.HasPrefix(path, gitPrefix) {
return nil
}

var conflictReslv mergeConflictResolve
finfo, _ := d.Info()
entry := &fsEntry{Prefix: pkgPath, Path: path, Entry: finfo, Excluded: false, From: pkgName}

if !ok {
// No strategies for package. Proceed with default merge.
entriesTree, conflictReslv = addEntries(entriesTree, entriesMap, entry, path)
} else {
entriesTree, conflictReslv = addStrategyEntries(strategies, entriesTree, entriesMap, entry, path)
}

if b.logConflicts && !finfo.IsDir() {
logConflictResolve(conflictReslv, path, pkgName, entriesMap[path])
}

// Skip .git folder from packages
if strings.HasPrefix(path, gitPrefix) {
return nil
}

var conflictReslv mergeConflictResolve
finfo, _ := d.Info()
entry := &fsEntry{Prefix: pkgPath, Path: path, Entry: finfo, Excluded: false, From: pkgName}

if !ok {
// No strategies for package. Proceed with default merge.
entriesTree, conflictReslv = addEntries(entriesTree, entriesMap, entry, path)
} else {
entriesTree, conflictReslv = addStrategyEntries(strategies, entriesTree, entriesMap, entry, path)
}
})

if b.logConflicts && !finfo.IsDir() {
logConflictResolve(conflictReslv, path, pkgName, entriesMap[path])
if err != nil {
return err
}

return nil
})

if err != nil {
return err
}
}
}

// @todo check rsync
for _, treeItem := range entriesTree {
sourcePath := filepath.Join(treeItem.Prefix, treeItem.Path)
destPath := filepath.Join(b.targetDir, treeItem.Path)
isSymlink := false
permissions := os.FileMode(dirPermissions)

switch treeItem.Entry.Mode() & os.ModeType {
case os.ModeDir:
if err := createDir(destPath, treeItem.Entry.Mode()); err != nil {
return err
}
case os.ModeSymlink:
if err := lcopy(sourcePath, destPath); err != nil {
return err
}
isSymlink = true
select {
case <-ctx.Done():
return ctx.Err()
default:
permissions = treeItem.Entry.Mode()
if err := fcopy(sourcePath, destPath); err != nil {
return err
sourcePath := filepath.Join(treeItem.Prefix, treeItem.Path)
destPath := filepath.Join(b.targetDir, treeItem.Path)
isSymlink := false
permissions := os.FileMode(dirPermissions)

switch treeItem.Entry.Mode() & os.ModeType {
case os.ModeDir:
if err := createDir(destPath, treeItem.Entry.Mode()); err != nil {
return err
}
case os.ModeSymlink:
if err := lcopy(sourcePath, destPath); err != nil {
return err
}
isSymlink = true
default:
permissions = treeItem.Entry.Mode()
if err := fcopy(sourcePath, destPath); err != nil {
return err
}
}
}

if !isSymlink {
if err := os.Chmod(destPath, permissions); err != nil {
return err
if !isSymlink {
if err := os.Chmod(destPath, permissions); err != nil {
return err
}
}
}
}
Expand Down
Loading
Loading