Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Composability by component #385

Merged
merged 7 commits into from
Mar 14, 2022
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
127 changes: 84 additions & 43 deletions cli/internal/packager/compose.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package packager

import (
"strings"

"github.com/defenseunicorns/zarf/cli/config"
"github.com/defenseunicorns/zarf/cli/internal/message"
"github.com/defenseunicorns/zarf/cli/internal/packager/validate"
Expand All @@ -11,61 +13,68 @@ import (
func GetComposedComponents() (components []types.ZarfComponent) {
for _, component := range config.GetComponents() {
// Check for standard component.
if !hasComposedPackage(&component) {
if component.Import.Path == "" {
// Append standard component to list.
components = append(components, component)
} else if shouldComposePackage(&component) { // Validate and confirm inclusion of imported package.
} else {
validateOrBail(&component)

// Expand and add components from imported package.
importedComponents := getSubPackageAssets(component)
components = append(components, importedComponents...)
importedComponent := getImportedComponent(component)
// Merge in parent component changes.
mergeComponentOverrides(&importedComponent, component)
// Add to the list of components for the package.
components = append(components, importedComponent)
}
}

// Update the parent package config with the expanded sub components.
// This is important when the deploy package is created.
config.SetComponents(components)
return components
}

// Returns true if import field is populated.
func hasComposedPackage(component *types.ZarfComponent) bool {
return component.Import != types.ZarfImport{}
}

// Validates and confirms inclusion of imported package.
func shouldComposePackage(component *types.ZarfComponent) bool {
validateOrBail(component)
return componentConfirmedForInclusion(component)
}

// Returns true if confirm flag is true, the component is required, or the user confirms inclusion.
func componentConfirmedForInclusion(component *types.ZarfComponent) bool {
return config.DeployOptions.Confirm || component.Required || ConfirmOptionalComponent(*component)
}

// Validates the sub component, exits program if validation fails.
func validateOrBail(component *types.ZarfComponent) {
if err := validate.ValidateImportPackage(component); err != nil {
message.Fatalf(err, "Invalid import definition in the %s component: %s", component.Name, err)
}
}

// Sets Name, Default, Required, Description and SecretName to the original components values
func mergeComponentOverrides(target *types.ZarfComponent, src types.ZarfComponent) {
target.Name = src.Name
target.Default = src.Default
target.Required = src.Required

if src.Description != "" {
target.Description = src.Description
}

if src.SecretName != "" {
target.SecretName = src.SecretName
}
}

// Get expanded components from imported component.
func getSubPackageAssets(importComponent types.ZarfComponent) (components []types.ZarfComponent) {
func getImportedComponent(importComponent types.ZarfComponent) (component types.ZarfComponent) {
// Read the imported package.
importedPackage := getSubPackage(&importComponent)
// Iterate imported components.

componentName := importComponent.Import.ComponentName
// Default to the component name if a custom one was not provided
if componentName == "" {
componentName = importComponent.Name
}

// Loop over package components looking for a match the componentName
for _, componentToCompose := range importedPackage.Components {
// Check for standard component.
if !hasComposedPackage(&componentToCompose) {
// Doctor standard component name and included files.
prepComponentToCompose(&componentToCompose, importedPackage.Metadata.Name, importComponent.Import.Path)
components = append(components, componentToCompose)
} else if shouldComposePackage(&componentToCompose) {
// Recurse on imported components.
components = append(components, getSubPackageAssets(componentToCompose)...)
if componentToCompose.Name == componentName {
return *prepComponentToCompose(&componentToCompose, importComponent)
}
}
return components

return component
}

// Reads the locally imported zarf.yaml
Expand All @@ -74,32 +83,38 @@ func getSubPackage(component *types.ZarfComponent) (importedPackage types.ZarfPa
return importedPackage
}

// Updates the name and sets all local asset paths relative to the importing package.
func prepComponentToCompose(component *types.ZarfComponent, parentPackageName string, importPath string) {
// Prefix component name with parent package name to distinguish similarly named components.
component.Name = parentPackageName + "-" + component.Name
// Updates the name and sets all local asset paths relative to the importing component.
func prepComponentToCompose(child *types.ZarfComponent, parent types.ZarfComponent) *types.ZarfComponent {

if child.Import.Path != "" {
// The component we are trying to compose is a composed component itself!
nestedComponent := getImportedComponent(*child)
child = prepComponentToCompose(&nestedComponent, *child)
}

// Prefix composed component file paths.
for fileIdx, file := range component.Files {
component.Files[fileIdx].Source = getComposedFilePath(file.Source, importPath)
for fileIdx, file := range child.Files {
child.Files[fileIdx].Source = getComposedFilePath(file.Source, parent.Import.Path)
}

// Prefix non-url composed component chart values files.
for chartIdx, chart := range component.Charts {
for chartIdx, chart := range child.Charts {
for valuesIdx, valuesFile := range chart.ValuesFiles {
component.Charts[chartIdx].ValuesFiles[valuesIdx] = getComposedFilePath(valuesFile, importPath)
child.Charts[chartIdx].ValuesFiles[valuesIdx] = getComposedFilePath(valuesFile, parent.Import.Path)
}
}

// Prefix non-url composed manifest files and kustomizations.
for manifestIdx, manifest := range component.Manifests {
for manifestIdx, manifest := range child.Manifests {
for fileIdx, file := range manifest.Files {
component.Manifests[manifestIdx].Files[fileIdx] = getComposedFilePath(file, importPath)
child.Manifests[manifestIdx].Files[fileIdx] = getComposedFilePath(file, parent.Import.Path)
}
for kustomIdx, kustomization := range manifest.Kustomizations {
component.Manifests[manifestIdx].Kustomizations[kustomIdx] = getComposedFilePath(kustomization, importPath)
child.Manifests[manifestIdx].Kustomizations[kustomIdx] = getComposedFilePath(kustomization, parent.Import.Path)
}
}

return child
}

// Prefix file path with importPath if original file path is not a url.
Expand All @@ -109,5 +124,31 @@ func getComposedFilePath(originalPath string, pathPrefix string) string {
return originalPath
}
// Add prefix for local files.
return pathPrefix + originalPath
return fixRelativePathBacktracking(pathPrefix + originalPath)
}

func fixRelativePathBacktracking(path string) string {
var newPathBuilder []string
var hitRealPath = false // We might need to go back several directories at the begining

// Turn paths like `../../this/is/a/very/../silly/../path` into `../../this/is/a/path`
splitString := strings.Split(path, "/")
for _, dir := range splitString {
if dir == ".." {
if hitRealPath {
// Instead of going back a directory, just don't get here in the first place
newPathBuilder = newPathBuilder[:len(newPathBuilder)-1]
} else {
// We are still going back directories for the first time, keep going back
newPathBuilder = append(newPathBuilder, dir)
}
} else {
// This is a regular directory we want to travel through
hitRealPath = true
newPathBuilder = append(newPathBuilder, dir)
}
}

// NOTE: This assumes a relative path
return strings.Join(newPathBuilder, "/")
}
17 changes: 9 additions & 8 deletions cli/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ type ZarfComponent struct {
// Scripts are custom commands that run before or after package deployment
Scripts ZarfComponentScripts `yaml:"scripts,omitempty"`

// Import refers to another zarf.yaml package.
Import ZarfImport `yaml:"import,omitempty"`
// Import refers to another zarf.yaml package component.
Import ZarfComponentImport `yaml:"import,omitempty"`
}

// ZarfManifest defines raw manifests Zarf will deploy as a helm chart
Expand Down Expand Up @@ -139,14 +139,15 @@ type TLSConfig struct {

// ZarfDeployOptions tracks the user-defined preferences during a package deployment
type ZarfDeployOptions struct {
PackagePath string
Confirm bool
Components string
PackagePath string
Confirm bool
Components string
// Zarf init is installing the k3s component
ApplianceMode bool
}

// ZarfImport structure for including imported zarf packages
type ZarfImport struct {
Path string `yaml:"path"`
// ZarfImport structure for including imported zarf components
type ZarfComponentImport struct {
ComponentName string `yaml:"name,omitempty"`
Path string `yaml:"path"`
}
7 changes: 5 additions & 2 deletions examples/composable-packages/zarf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@ metadata:
components:
- name: games
required: true
import:
path: '../game/'
description: "Example of a composed package with a unique description for this component"
import:
path: ../game
# Example optional custom name to point to in the imported package
name: baseline
3 changes: 0 additions & 3 deletions test/e2e/e2e_composability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ func TestE2eExampleComposability(t *testing.T) {
output, err = e2e.execZarfCommand("package", "deploy", "../../build/zarf-package-compose-example.tar.zst", "--confirm")
require.NoError(t, err, output)

// Validate that the composed sub packages exist
require.Contains(t, output, "appliance-demo-multi-games-baseline")

// Establish the port-forward into the game service
err = e2e.execZarfBackgroundCommand("connect", "doom", "--local-port=22333")
require.NoError(t, err, "unable to connect to the doom port-forward")
Expand Down
29 changes: 16 additions & 13 deletions zarf.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,22 @@
},
"import": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/ZarfImport"
"$ref": "#/definitions/ZarfComponentImport"
}
},
"additionalProperties": false,
"type": "object"
},
"ZarfComponentImport": {
"required": [
"path"
],
"properties": {
"name": {
"type": "string"
},
"path": {
"type": "string"
}
},
"additionalProperties": false,
Expand Down Expand Up @@ -217,18 +232,6 @@
"additionalProperties": false,
"type": "object"
},
"ZarfImport": {
"required": [
"path"
],
"properties": {
"path": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object"
},
"ZarfManifest": {
"required": [
"name"
Expand Down