github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/pkg/packager/composer/list.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // SPDX-FileCopyrightText: 2021-Present The Jackal Authors
     3  
     4  // Package composer contains functions for composing components within Jackal packages.
     5  package composer
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"path/filepath"
    11  	"strings"
    12  
    13  	"github.com/Racer159/jackal/src/internal/packager/validate"
    14  	"github.com/Racer159/jackal/src/pkg/layout"
    15  	"github.com/Racer159/jackal/src/pkg/packager/deprecated"
    16  	"github.com/Racer159/jackal/src/pkg/utils"
    17  	"github.com/Racer159/jackal/src/pkg/zoci"
    18  	"github.com/Racer159/jackal/src/types"
    19  	"github.com/defenseunicorns/pkg/helpers"
    20  )
    21  
    22  // Node is a node in the import chain
    23  type Node struct {
    24  	types.JackalComponent
    25  
    26  	index int
    27  
    28  	vars   []types.JackalPackageVariable
    29  	consts []types.JackalPackageConstant
    30  
    31  	relativeToHead      string
    32  	originalPackageName string
    33  
    34  	prev *Node
    35  	next *Node
    36  }
    37  
    38  // Index returns the .components index location for this node's source `jackal.yaml`
    39  func (n *Node) Index() int {
    40  	return n.index
    41  }
    42  
    43  // OriginalPackageName returns the .metadata.name for this node's source `jackal.yaml`
    44  func (n *Node) OriginalPackageName() string {
    45  	return n.originalPackageName
    46  }
    47  
    48  // ImportLocation gets the path from the base `jackal.yaml` to the imported `jackal.yaml`
    49  func (n *Node) ImportLocation() string {
    50  	if n.prev != nil {
    51  		if n.prev.JackalComponent.Import.URL != "" {
    52  			return n.prev.JackalComponent.Import.URL
    53  		}
    54  	}
    55  	return n.relativeToHead
    56  }
    57  
    58  // Next returns next node in the chain
    59  func (n *Node) Next() *Node {
    60  	return n.next
    61  }
    62  
    63  // Prev returns previous node in the chain
    64  func (n *Node) Prev() *Node {
    65  	return n.prev
    66  }
    67  
    68  // ImportName returns the name of the component to import
    69  // If the component import has a ComponentName defined, that will be used
    70  // otherwise the name of the component will be used
    71  func (n *Node) ImportName() string {
    72  	name := n.JackalComponent.Name
    73  	if n.Import.ComponentName != "" {
    74  		name = n.Import.ComponentName
    75  	}
    76  	return name
    77  }
    78  
    79  // ImportChain is a doubly linked list of component import definitions
    80  type ImportChain struct {
    81  	head *Node
    82  	tail *Node
    83  
    84  	remote *zoci.Remote
    85  }
    86  
    87  // Head returns the first node in the import chain
    88  func (ic *ImportChain) Head() *Node {
    89  	return ic.head
    90  }
    91  
    92  // Tail returns the last node in the import chain
    93  func (ic *ImportChain) Tail() *Node {
    94  	return ic.tail
    95  }
    96  
    97  func (ic *ImportChain) append(c types.JackalComponent, index int, originalPackageName string,
    98  	relativeToHead string, vars []types.JackalPackageVariable, consts []types.JackalPackageConstant) {
    99  	node := &Node{
   100  		JackalComponent:     c,
   101  		index:               index,
   102  		originalPackageName: originalPackageName,
   103  		relativeToHead:      relativeToHead,
   104  		vars:                vars,
   105  		consts:              consts,
   106  		prev:                nil,
   107  		next:                nil,
   108  	}
   109  	if ic.head == nil {
   110  		ic.head = node
   111  		ic.tail = node
   112  	} else {
   113  		p := ic.tail
   114  		node.prev = p
   115  		p.next = node
   116  		ic.tail = node
   117  	}
   118  }
   119  
   120  // NewImportChain creates a new import chain from a component
   121  // Returning the chain on error so we can have additional information to use during lint
   122  func NewImportChain(head types.JackalComponent, index int, originalPackageName, arch, flavor string) (*ImportChain, error) {
   123  	ic := &ImportChain{}
   124  	if arch == "" {
   125  		return ic, fmt.Errorf("cannot build import chain: architecture must be provided")
   126  	}
   127  
   128  	ic.append(head, index, originalPackageName, ".", nil, nil)
   129  
   130  	history := []string{}
   131  
   132  	node := ic.head
   133  	for node != nil {
   134  		isLocal := node.Import.Path != ""
   135  		isRemote := node.Import.URL != ""
   136  
   137  		if !isLocal && !isRemote {
   138  			// This is the end of the import chain,
   139  			// as the current node/component is not importing anything
   140  			return ic, nil
   141  		}
   142  
   143  		// TODO: stuff like this should also happen in linting
   144  		if err := validate.ImportDefinition(&node.JackalComponent); err != nil {
   145  			return ic, err
   146  		}
   147  
   148  		// ensure that remote components are not importing other remote components
   149  		if node.prev != nil && node.prev.Import.URL != "" && isRemote {
   150  			return ic, fmt.Errorf("detected malformed import chain, cannot import remote components from remote components")
   151  		}
   152  		// ensure that remote components are not importing local components
   153  		if node.prev != nil && node.prev.Import.URL != "" && isLocal {
   154  			return ic, fmt.Errorf("detected malformed import chain, cannot import local components from remote components")
   155  		}
   156  
   157  		var pkg types.JackalPackage
   158  
   159  		var relativeToHead string
   160  		var importURL string
   161  		if isLocal {
   162  			history = append(history, node.Import.Path)
   163  			relativeToHead = filepath.Join(history...)
   164  
   165  			// prevent circular imports (including self-imports)
   166  			// this is O(n^2) but the import chain should be small
   167  			prev := node
   168  			for prev != nil {
   169  				if prev.relativeToHead == relativeToHead {
   170  					return ic, fmt.Errorf("detected circular import chain: %s", strings.Join(history, " -> "))
   171  				}
   172  				prev = prev.prev
   173  			}
   174  
   175  			// this assumes the composed package is following the jackal layout
   176  			if err := utils.ReadYaml(filepath.Join(relativeToHead, layout.JackalYAML), &pkg); err != nil {
   177  				return ic, err
   178  			}
   179  		} else if isRemote {
   180  			importURL = node.Import.URL
   181  			remote, err := ic.getRemote(node.Import.URL)
   182  			if err != nil {
   183  				return ic, err
   184  			}
   185  			pkg, err = remote.FetchJackalYAML(context.TODO())
   186  			if err != nil {
   187  				return ic, err
   188  			}
   189  		}
   190  
   191  		name := node.ImportName()
   192  
   193  		// 'found' and 'index' are parallel slices. Each element in found[x] corresponds to pkg[index[x]]
   194  		// found[0] and pkg[index[0]] would be the same component for example
   195  		found := []types.JackalComponent{}
   196  		index := []int{}
   197  		for i, component := range pkg.Components {
   198  			if component.Name == name && CompatibleComponent(component, arch, flavor) {
   199  				found = append(found, component)
   200  				index = append(index, i)
   201  			}
   202  		}
   203  
   204  		if len(found) == 0 {
   205  			componentNotFound := "component %q not found in %q"
   206  			if isLocal {
   207  				return ic, fmt.Errorf(componentNotFound, name, relativeToHead)
   208  			} else if isRemote {
   209  				return ic, fmt.Errorf(componentNotFound, name, importURL)
   210  			}
   211  		} else if len(found) > 1 {
   212  			multipleComponentsFound := "multiple components named %q found in %q satisfying %q"
   213  			if isLocal {
   214  				return ic, fmt.Errorf(multipleComponentsFound, name, relativeToHead, arch)
   215  			} else if isRemote {
   216  				return ic, fmt.Errorf(multipleComponentsFound, name, importURL, arch)
   217  			}
   218  		}
   219  
   220  		ic.append(found[0], index[0], pkg.Metadata.Name, relativeToHead, pkg.Variables, pkg.Constants)
   221  		node = node.next
   222  	}
   223  	return ic, nil
   224  }
   225  
   226  // String returns a string representation of the import chain
   227  func (ic *ImportChain) String() string {
   228  	if ic.head.next == nil {
   229  		return fmt.Sprintf("component %q imports nothing", ic.head.Name)
   230  	}
   231  
   232  	s := strings.Builder{}
   233  
   234  	name := ic.head.ImportName()
   235  
   236  	if ic.head.Import.Path != "" {
   237  		s.WriteString(fmt.Sprintf("component %q imports %q in %s", ic.head.Name, name, ic.head.Import.Path))
   238  	} else {
   239  		s.WriteString(fmt.Sprintf("component %q imports %q in %s", ic.head.Name, name, ic.head.Import.URL))
   240  	}
   241  
   242  	node := ic.head.next
   243  	for node != ic.tail {
   244  		name := node.ImportName()
   245  		s.WriteString(", which imports ")
   246  		if node.Import.Path != "" {
   247  			s.WriteString(fmt.Sprintf("%q in %s", name, node.Import.Path))
   248  		} else {
   249  			s.WriteString(fmt.Sprintf("%q in %s", name, node.Import.URL))
   250  		}
   251  
   252  		node = node.next
   253  	}
   254  
   255  	return s.String()
   256  }
   257  
   258  // Migrate performs migrations on the import chain
   259  func (ic *ImportChain) Migrate(build types.JackalBuildData) (warnings []string) {
   260  	node := ic.head
   261  	for node != nil {
   262  		migrated, w := deprecated.MigrateComponent(build, node.JackalComponent)
   263  		node.JackalComponent = migrated
   264  		warnings = append(warnings, w...)
   265  		node = node.next
   266  	}
   267  	if len(warnings) > 0 {
   268  		final := fmt.Sprintf("Migrations were performed on the import chain of: %q", ic.head.Name)
   269  		warnings = append(warnings, final)
   270  	}
   271  	return warnings
   272  }
   273  
   274  // Compose merges the import chain into a single component
   275  // fixing paths, overriding metadata, etc
   276  func (ic *ImportChain) Compose() (composed *types.JackalComponent, err error) {
   277  	composed = &ic.tail.JackalComponent
   278  
   279  	if ic.tail.prev == nil {
   280  		// only had one component in the import chain
   281  		return composed, nil
   282  	}
   283  
   284  	if err := ic.fetchOCISkeleton(); err != nil {
   285  		return nil, err
   286  	}
   287  
   288  	// start with an empty component to compose into
   289  	composed = &types.JackalComponent{}
   290  
   291  	// start overriding with the tail node
   292  	node := ic.tail
   293  	for node != nil {
   294  		fixPaths(&node.JackalComponent, node.relativeToHead)
   295  
   296  		// perform overrides here
   297  		err := overrideMetadata(composed, node.JackalComponent)
   298  		if err != nil {
   299  			return nil, err
   300  		}
   301  
   302  		overrideDeprecated(composed, node.JackalComponent)
   303  		overrideResources(composed, node.JackalComponent)
   304  		overrideActions(composed, node.JackalComponent)
   305  
   306  		composeExtensions(composed, node.JackalComponent, node.relativeToHead)
   307  
   308  		node = node.prev
   309  	}
   310  
   311  	return composed, nil
   312  }
   313  
   314  // MergeVariables merges variables from the import chain
   315  func (ic *ImportChain) MergeVariables(existing []types.JackalPackageVariable) (merged []types.JackalPackageVariable) {
   316  	exists := func(v1 types.JackalPackageVariable, v2 types.JackalPackageVariable) bool {
   317  		return v1.Name == v2.Name
   318  	}
   319  
   320  	node := ic.tail
   321  	for node != nil {
   322  		// merge the vars
   323  		merged = helpers.MergeSlices(node.vars, merged, exists)
   324  		node = node.prev
   325  	}
   326  	merged = helpers.MergeSlices(existing, merged, exists)
   327  
   328  	return merged
   329  }
   330  
   331  // MergeConstants merges constants from the import chain
   332  func (ic *ImportChain) MergeConstants(existing []types.JackalPackageConstant) (merged []types.JackalPackageConstant) {
   333  	exists := func(c1 types.JackalPackageConstant, c2 types.JackalPackageConstant) bool {
   334  		return c1.Name == c2.Name
   335  	}
   336  
   337  	node := ic.tail
   338  	for node != nil {
   339  		// merge the consts
   340  		merged = helpers.MergeSlices(node.consts, merged, exists)
   341  		node = node.prev
   342  	}
   343  	merged = helpers.MergeSlices(existing, merged, exists)
   344  
   345  	return merged
   346  }
   347  
   348  // CompatibleComponent determines if this component is compatible with the given create options
   349  func CompatibleComponent(c types.JackalComponent, arch, flavor string) bool {
   350  	satisfiesArch := c.Only.Cluster.Architecture == "" || c.Only.Cluster.Architecture == arch
   351  	satisfiesFlavor := c.Only.Flavor == "" || c.Only.Flavor == flavor
   352  	return satisfiesArch && satisfiesFlavor
   353  }