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 }