airshipctl/pkg/document/bundle.go

395 lines
12 KiB
Go

package document
import (
"errors"
"fmt"
"io"
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
"sigs.k8s.io/kustomize/v3/k8sdeps/transformer"
"sigs.k8s.io/kustomize/v3/k8sdeps/validator"
"sigs.k8s.io/kustomize/v3/pkg/loader"
"sigs.k8s.io/kustomize/v3/pkg/plugins"
"sigs.k8s.io/kustomize/v3/pkg/resmap"
"sigs.k8s.io/kustomize/v3/pkg/resource"
"sigs.k8s.io/kustomize/v3/pkg/target"
"sigs.k8s.io/kustomize/v3/pkg/types"
"sigs.k8s.io/kustomize/v3/plugin/builtin"
docplugins "opendev.org/airship/airshipctl/pkg/document/plugins"
"opendev.org/airship/airshipctl/pkg/log"
utilyaml "opendev.org/airship/airshipctl/pkg/util/yaml"
)
func init() {
// NOTE (dukov) This is sort of a hack but it's the only way to add an
// external 'builtin' plugin to Kustomize
plugins.TransformerFactories[plugins.Unknown] = docplugins.NewTransformerLoader
}
// KustomizeBuildOptions contain the options for running a Kustomize build on a bundle
type KustomizeBuildOptions struct {
KustomizationPath string
OutputPath string
LoadRestrictor loader.LoadRestrictorFunc
}
// BundleFactory contains the objects within a bundle
type BundleFactory struct {
KustomizeBuildOptions
resmap.ResMap
FileSystem
}
// Bundle interface provides the specification for a bundle implementation
type Bundle interface {
Write(out io.Writer) error
SetFileSystem(FileSystem) error
GetFileSystem() FileSystem
Select(selector Selector) ([]Document, error)
SelectOne(selector Selector) (Document, error)
SelectBundle(selector Selector) (Bundle, error)
SelectByFieldValue(string, func(interface{}) bool) (Bundle, error)
GetByGvk(string, string, string) ([]Document, error)
GetByName(string) (Document, error)
GetByAnnotation(annotationSelector string) ([]Document, error)
GetByLabel(labelSelector string) ([]Document, error)
GetAllDocuments() ([]Document, error)
}
// NewBundleByPath helper function that returns new document.Bundle interface based on clusterType and
// phase, example: helpers.NewBunde(airConfig, "ephemeral", "initinfra")
func NewBundleByPath(rootPath string) (Bundle, error) {
return NewBundle(NewDocumentFs(), rootPath, "")
}
// NewBundle is a convenience function to create a new bundle
// Over time, it will evolve to support allowing more control
// for kustomize plugins
func NewBundle(fSys FileSystem, kustomizePath string, outputPath string) (Bundle, error) {
var options = KustomizeBuildOptions{
KustomizationPath: kustomizePath,
OutputPath: outputPath,
LoadRestrictor: loader.RestrictionRootOnly,
}
// init an empty bundle factory
bundle := &BundleFactory{}
// set the fs and build options we will use
if err := bundle.SetFileSystem(fSys); err != nil {
return nil, err
}
if err := bundle.SetKustomizeBuildOptions(options); err != nil {
return nil, err
}
// boiler plate to allow us to run Kustomize build
uf := kunstruct.NewKunstructuredFactoryImpl()
pf := transformer.NewFactoryImpl()
rf := resmap.NewFactory(resource.NewFactory(uf), pf)
v := validator.NewKustValidator()
pluginConfig := plugins.DefaultPluginConfig()
pl := plugins.NewLoader(pluginConfig, rf)
ldr, err := loader.NewLoader(
bundle.GetKustomizeBuildOptions().LoadRestrictor, v, bundle.GetKustomizeBuildOptions().KustomizationPath, fSys)
if err != nil {
return bundle, err
}
defer func() {
if e := ldr.Cleanup(); e != nil {
log.Fatal("failed to cleanup loader ERROR: ", e)
}
}()
kt, err := target.NewKustTarget(ldr, rf, pf, pl)
if err != nil {
return bundle, err
}
// build a resource map of kustomize rendered objects
m, err := kt.MakeCustomizedResMap()
if err != nil {
return bundle, err
}
err = bundle.SetKustomizeResourceMap(m)
if err != nil {
return nil, err
}
err = bundle.OrderLegacy()
return bundle, err
}
// GetKustomizeResourceMap returns a Kustomize Resource Map for this bundle
func (b *BundleFactory) GetKustomizeResourceMap() resmap.ResMap {
return b.ResMap
}
// SetKustomizeResourceMap allows us to set the populated resource map for this bundle. In
// the future, it may modify it before saving it.
func (b *BundleFactory) SetKustomizeResourceMap(r resmap.ResMap) error {
b.ResMap = r
return nil
}
// GetKustomizeBuildOptions returns the build options object used to generate the resource map
// for this bundle
func (b *BundleFactory) GetKustomizeBuildOptions() KustomizeBuildOptions {
return b.KustomizeBuildOptions
}
// SetKustomizeBuildOptions sets the build options to be used for this bundle. In
// the future, it may perform some basic validations.
func (b *BundleFactory) SetKustomizeBuildOptions(k KustomizeBuildOptions) error {
b.KustomizeBuildOptions = k
return nil
}
// SetFileSystem sets the filesystem that will be used by this bundle
func (b *BundleFactory) SetFileSystem(fSys FileSystem) error {
b.FileSystem = fSys
return nil
}
// GetFileSystem gets the filesystem that will be used by this bundle
func (b *BundleFactory) GetFileSystem() FileSystem {
return b.FileSystem
}
// OrderLegacy uses kustomize Transformer plugin to correct order of resources
func (b *BundleFactory) OrderLegacy() error {
return builtin.NewLegacyOrderTransformerPlugin().Transform(b.GetKustomizeResourceMap())
}
// GetAllDocuments returns all documents in this bundle
func (b *BundleFactory) GetAllDocuments() ([]Document, error) {
docSet := make([]Document, len(b.ResMap.Resources()))
for i, res := range b.ResMap.Resources() {
// Construct Bundle document for each resource returned
doc, err := NewDocument(res)
if err != nil {
return docSet, err
}
docSet[i] = doc
}
return docSet, nil
}
// GetByName finds a document by name, error if more than one document found
// or if no documents found
func (b *BundleFactory) GetByName(name string) (Document, error) {
resSet := make([]*resource.Resource, 0, len(b.ResMap.Resources()))
for _, res := range b.ResMap.Resources() {
if res.GetName() == name {
resSet = append(resSet, res)
}
}
// alanmeadows(TODO): improve this and other error potentials by
// by adding strongly typed errors
switch found := len(resSet); {
case found == 0:
return &Factory{}, fmt.Errorf("no documents found with name %s", name)
case found > 1:
return &Factory{}, fmt.Errorf("more than one document found with name %s", name)
default:
return NewDocument(resSet[0])
}
}
// Select offers an interface to pass a Selector, built on top of kustomize Selector
// to the bundle returning Documents that match the criteria
func (b *BundleFactory) Select(selector Selector) ([]Document, error) {
// use the kustomize select method
resources, err := b.ResMap.Select(selector.Selector)
if err != nil {
return []Document{}, err
}
// Construct Bundle document for each resource returned
docSet := make([]Document, len(resources))
for i, res := range resources {
var doc Document
doc, err = NewDocument(res)
if err != nil {
return docSet, err
}
docSet[i] = doc
}
return docSet, err
}
// SelectOne serves the common use case where you expect one match
// and only one match to your selector -- in other words, you want to
// error if you didn't find any documents, and error if you found
// more than one. This reduces code repetition that would otherwise
// be scattered around that evaluates the length of the doc set returned
// for this common case
func (b *BundleFactory) SelectOne(selector Selector) (Document, error) {
docSet, err := b.Select(selector)
if err != nil {
return nil, err
}
// evaluate docSet for at least one document, and no more than
// one document and raise errors as appropriate
switch numDocsFound := len(docSet); {
case numDocsFound == 0:
return nil, ErrDocNotFound{Selector: selector}
case numDocsFound > 1:
return nil, ErrMultipleDocsFound{Selector: selector}
}
return docSet[0], nil
}
// SelectBundle offers an interface to pass a Selector, built on top of kustomize Selector
// to the bundle returning a new Bundle that matches the criteria. This is useful
// where you want to actually prune the underlying bundle you are working with
// rather then getting back the matching documents for scenarios like
// test cases where you want to pass in custom "filtered" bundles
// specific to the test case
func (b *BundleFactory) SelectBundle(selector Selector) (Bundle, error) {
// use the kustomize select method
resources, err := b.ResMap.Select(selector.Selector)
if err != nil {
return nil, err
}
// create a blank resourcemap and append the found resources
// into the new resource map
resourceMap := resmap.New()
for _, res := range resources {
if err = resourceMap.Append(res); err != nil {
return nil, err
}
}
// return a new bundle with the same options and filesystem
// as this one but with a reduced resourceMap
return &BundleFactory{
KustomizeBuildOptions: b.KustomizeBuildOptions,
ResMap: resourceMap,
FileSystem: b.FileSystem,
}, nil
}
// SelectByFieldValue returns new Bundle with filtered resource documents.
// Method iterates over all resources in the bundle. If resource has field
// (i.e. key) specified in JSON path, and the comparison function returns
// 'true' for value referenced by JSON path, then resource is added to
// resulting bundle.
// Example:
// The bundle contains 3 documents
//
// ---
// apiVersion: v1
// kind: DocKind1
// metadata:
// name: doc1
// spec:
// somekey:
// somefield: "someValue"
// ---
// apiVersion: v1
// kind: DocKind2
// metadata:
// name: doc2
// spec:
// somekey:
// somefield: "someValue"
// ---
// apiVersion: v1
// kind: DocKind1
// metadata:
// name: doc3
// spec:
// somekey:
// somefield: "someOtherValue"
//
// Execution of bundleInstance.SelectByFieldValue(
// "spec.somekey.somefield",
// func(v interface{}) { return v == "someValue" })
// will return a new Bundle instance containing 2 documents:
// ---
// apiVersion: v1
// kind: DocKind1
// metadata:
// name: doc1
// spec:
// somekey:
// somefield: "someValue"
// ---
// apiVersion: v1
// kind: DocKind2
// metadata:
// name: doc2
// spec:
// somekey:
// somefield: "someValue"
func (b *BundleFactory) SelectByFieldValue(path string, condition func(interface{}) bool) (Bundle, error) {
result := &BundleFactory{
KustomizeBuildOptions: b.KustomizeBuildOptions,
FileSystem: b.FileSystem,
}
resourceMap := resmap.New()
for _, res := range b.Resources() {
val, err := res.GetFieldValue(path)
if err != nil {
if errors.As(err, &types.NoFieldError{}) {
// this resource doesn't have the specified field - skip it
continue
} else {
return nil, err
}
}
if condition(val) {
if err = resourceMap.Append(res); err != nil {
return nil, err
}
}
}
if err := result.SetKustomizeResourceMap(resourceMap); err != nil {
return nil, err
}
return result, nil
}
// GetByAnnotation is a convenience method to get documents for a particular annotation
func (b *BundleFactory) GetByAnnotation(annotationSelector string) ([]Document, error) {
// Construct kustomize annotation selector
selector := NewSelector().ByAnnotation(annotationSelector)
// pass it to the selector
return b.Select(selector)
}
// GetByLabel is a convenience method to get documents for a particular label
func (b *BundleFactory) GetByLabel(labelSelector string) ([]Document, error) {
// Construct kustomize label selector
selector := NewSelector().ByLabel(labelSelector)
// pass it to the selector
return b.Select(selector)
}
// GetByGvk is a convenience method to get documents for a particular Gvk tuple
func (b *BundleFactory) GetByGvk(group, version, kind string) ([]Document, error) {
// Construct kustomize gvk object
selector := NewSelector().ByGvk(group, version, kind)
// pass it to the selector
return b.Select(selector)
}
// Write will write out the entire bundle resource map
func (b *BundleFactory) Write(out io.Writer) error {
for _, res := range b.ResMap.Resources() {
err := utilyaml.WriteOut(out, res)
if err != nil {
return err
}
}
return nil
}