pkg/build/inline.go (374 lines of code) (raw):
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package build
import (
"context"
"fmt"
"net/url"
"path/filepath"
"regexp"
"strings"
bundle "github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/apis/bundle/v1alpha1"
"github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/converter"
"github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/files"
"github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/internal"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/validation"
)
// Inliner inlines data files by reading them from the local or a remote
// filesystem.
type Inliner struct {
// Readers reads from the local filesystem.
Readers map[files.URLScheme]files.FileObjReader
}
// NewLocalInliner creates a new inliner that only knows how to read local
// files from disk. If the data is stored on disk, the cwd should be the path
// to the directory containing the data file on disk. Relative paths are not
// supported.
func NewLocalInliner(cwd string) *Inliner {
return NewInlinerWithScheme(
files.FileScheme,
&files.LocalFileObjReader{
WorkingDir: filepath.Dir(cwd),
Rdr: &files.LocalFileSystemReader{},
},
)
}
// NewInlinerWithScheme creates a new inliner given a URL scheme.
func NewInlinerWithScheme(scheme files.URLScheme, objReader files.FileObjReader) *Inliner {
rdrMap := map[files.URLScheme]files.FileObjReader{
scheme: objReader,
}
return &Inliner{
Readers: rdrMap,
}
}
// BundleFiles inlines file-references in for bundle files. If
// the bundlePath is defined and not absolute and the scheme is file based
// scheme, then the path is made absolute before proceeding.
func (n *Inliner) BundleFiles(ctx context.Context, data *bundle.BundleBuilder, bundlePath string) (*bundle.Bundle, error) {
bundleURL, err := url.Parse(bundlePath)
if err != nil {
return nil, err
}
bundleURL, err = makeAbsForFileScheme(bundleURL)
if err != nil {
return nil, err
}
if !filepath.IsAbs(bundleURL.Path) {
return nil, fmt.Errorf("bundlePath must be absolute but was %s", bundleURL.Path)
}
var comps []*bundle.Component
for _, f := range data.ComponentFiles {
furl, err := f.ParsedURL()
if err != nil {
return nil, err
}
f.URL = makeAbsWithParent(bundleURL, furl).String()
contents, err := n.readFile(ctx, f)
if err != nil {
return nil, fmt.Errorf("error reading file %q: %v", f.URL, err)
}
uns, err := converter.FromFileName(f.URL, contents).ToUnstructured()
if err != nil {
return nil, err
}
kind := uns.GetKind()
switch kind {
case "Component":
c, err := converter.FromFileName(f.URL, contents).ToComponent()
if err != nil {
return nil, err
}
comps = append(comps, c)
case "ComponentBuilder":
c, err := converter.FromFileName(f.URL, contents).ToComponentBuilder()
if err != nil {
return nil, err
}
if c.GetName() == "" && data.ComponentNamePolicy == "SetAndComponent" {
c.ObjectMeta.Name = strings.Join([]string{data.SetName, data.Version, c.ComponentName, c.Version}, "-")
}
comp, err := n.ComponentFiles(ctx, c, f.URL)
if err != nil {
return nil, err
}
comps = append(comps, comp)
default:
return nil, fmt.Errorf("unsupported kind for component: %q; only supported kinds are Component and ComponentBuilder", kind)
}
}
newBundle := &bundle.Bundle{
TypeMeta: metav1.TypeMeta{
APIVersion: "bundle.gke.io/v1alpha1",
Kind: "Bundle",
},
ObjectMeta: *data.ObjectMeta.DeepCopy(),
SetName: data.SetName,
Version: data.Version,
Components: comps,
}
return newBundle, nil
}
var onlyWhitespace = regexp.MustCompile(`^\s*$`)
var multiDoc = regexp.MustCompile("(^|\n)---")
var nonDNS = regexp.MustCompile(`[^-a-z0-9\.]`)
// ComponentFiles reads file-references for component builder objects. The
// returned components are copies with the file-references removed. If the
// componentPath is not absolute and the scheme is a file scheme, it will be
// made absolute before proceeding.
func (n *Inliner) ComponentFiles(ctx context.Context, comp *bundle.ComponentBuilder, componentPath string) (*bundle.Component, error) {
componentURL, err := url.Parse(componentPath)
if err != nil {
return nil, err
}
componentURL, err = makeAbsForFileScheme(componentURL)
if err != nil {
return nil, err
}
if !filepath.IsAbs(componentURL.Path) {
return nil, fmt.Errorf("componentURL must be absolute but was %s", componentURL.Path)
}
newObjs, tmplBuilders, err := n.objectFiles(ctx, comp.ObjectFiles, comp.ComponentReference(), componentURL)
if err != nil {
return nil, err
}
// tmplObjs from template builder
tmplObjs, err := n.objectTemplateBuilders(ctx, tmplBuilders, comp.ComponentReference())
if err != nil {
return nil, err
}
newObjs = append(newObjs, tmplObjs...)
// tmplObjs from template files
tmplObjs, err = n.templateFiles(ctx, comp.TemplateFiles, comp.ComponentReference(), componentURL, comp.ObjectMeta)
if err != nil {
return nil, err
}
newObjs = append(newObjs, tmplObjs...)
cfgObj, err := n.rawTextFiles(ctx, comp.RawTextFiles, comp.ComponentReference(), componentURL)
if err != nil {
return nil, err
}
newObjs = append(newObjs, cfgObj...)
om := *comp.ObjectMeta.DeepCopy()
if om.Name == "" {
name := strings.ToLower(comp.ComponentName + `-` + comp.Version)
om.Name = nonDNS.ReplaceAllLiteralString(name, `-`)
}
errs := validation.IsDNS1123Subdomain(om.Name)
if len(errs) > 0 {
return nil, fmt.Errorf("metadata.Name %q is not a valid DNS 1123 subdomain in component %q/%q: %v",
om.Name, comp.ComponentName, comp.Version, errs)
}
newComp := &bundle.Component{
TypeMeta: metav1.TypeMeta{
APIVersion: "bundle.gke.io/v1alpha1",
Kind: "Component",
},
ObjectMeta: om,
Spec: bundle.ComponentSpec{
ComponentName: comp.ComponentName,
Version: comp.Version,
Objects: newObjs,
},
}
return newComp, nil
}
// AllComponentFiles is a convenience method for inlining multiple component files.
func (n *Inliner) AllComponentFiles(ctx context.Context, cbs []*bundle.ComponentBuilder) ([]*bundle.Component, error) {
var out []*bundle.Component
for _, cb := range cbs {
newc, err := n.ComponentFiles(ctx, cb, "")
if err != nil {
return nil, err
}
out = append(out, newc)
}
return out, nil
}
// objectFiles inlines object files. in the success case, it returns
//
// 1.) The inlined object files.
// 2.) A map of path-to-ObjectTemplateBuilder
func (n *Inliner) objectFiles(ctx context.Context, objFiles []bundle.File, ref bundle.ComponentReference, componentPath *url.URL) ([]*unstructured.Unstructured, map[string][]*unstructured.Unstructured, error) {
var newObjs []*unstructured.Unstructured
objTmplBuilders := make(map[string][]*unstructured.Unstructured)
for _, cf := range objFiles {
furl, err := cf.ParsedURL()
if err != nil {
return nil, nil, err
}
cf.URL = makeAbsWithParent(componentPath, furl).String()
contents, err := n.readFile(ctx, cf)
if err != nil {
return nil, nil, fmt.Errorf("error reading file %v for component %v: %v", cf, ref, err)
}
ext := filepath.Ext(cf.URL)
if ext == ".yaml" && multiDoc.Match(contents) {
splat := multiDoc.Split(string(contents), -1)
for i, s := range splat {
if onlyWhitespace.MatchString(s) {
continue
}
obj, err := converter.FromYAMLString(s).ToUnstructured()
if err != nil {
return nil, nil, fmt.Errorf("converting multi-doc object number %d for component %v, %v", i, ref, err)
}
if obj.GetKind() == "ObjectTemplateBuilder" {
if objTmplBuilders[cf.URL] == nil {
objTmplBuilders[cf.URL] = []*unstructured.Unstructured{obj}
} else {
objTmplBuilders[cf.URL] = append(objTmplBuilders[cf.URL], obj)
}
} else {
newObjs = append(newObjs, obj)
}
}
} else {
obj, err := converter.FromFileName(cf.URL, contents).ToUnstructured()
if err != nil {
return nil, nil, fmt.Errorf("for component %q, %v", ref, err)
}
if obj.GetKind() == "ObjectTemplateBuilder" {
objTmplBuilders[cf.URL] = []*unstructured.Unstructured{obj}
} else {
newObjs = append(newObjs, obj)
}
}
}
return newObjs, objTmplBuilders, nil
}
// templateFiles reads template files and builds ObjectTemplates.
func (n *Inliner) templateFiles(ctx context.Context, tmplFiles []bundle.TemplateFileSet, ref bundle.ComponentReference, componentPath *url.URL, compMeta metav1.ObjectMeta) ([]*unstructured.Unstructured, error) {
var outObj []*unstructured.Unstructured
for _, tmplFileSet := range tmplFiles {
for _, tf := range tmplFileSet.Files {
furl, err := tf.ParsedURL()
if err != nil {
return nil, err
}
tf.URL = makeAbsWithParent(componentPath, furl).String()
contents, err := n.readFile(ctx, tf)
if err != nil {
return nil, fmt.Errorf("error reading file %v for component %v: %v", tf, ref, err)
}
// Note: metadata and optionsSchema are not supported with
// 'templateFiles' syntax.
objTemplate := &bundle.ObjectTemplate{
TypeMeta: metav1.TypeMeta{
APIVersion: "bundle.gke.io/v1alpha1",
Kind: "ObjectTemplate",
},
Template: string(contents),
}
objTemplate.ObjectMeta.Annotations = make(map[string]string)
objTemplate.ObjectMeta.Annotations[string(bundle.InlinePathIdentifier)] = tf.URL
if internal.HasSafeYAMLAnnotation(compMeta) {
objTemplate.ObjectMeta.Annotations[internal.SafeYAMLAnnotation] = compMeta.GetAnnotations()[internal.SafeYAMLAnnotation]
}
// templateType default is Go Template
tmplType := bundle.TemplateTypeGo
if tmplFileSet.TemplateType != bundle.TemplateTypeUndefined {
tmplType = tmplFileSet.TemplateType
}
objTemplate.Type = tmplType
objJSON, err := converter.FromObject(objTemplate).ToJSON()
if err != nil {
return nil, fmt.Errorf("for component %v and template file %q, while converting back to JSON: %v", ref, tf.URL, err)
}
unsObj, err := converter.FromJSON(objJSON).ToUnstructured()
if err != nil {
return nil, fmt.Errorf("for component %v and template file %q, while converting back to Unstructured: %v", ref, tf.URL, err)
}
outObj = append(outObj, unsObj)
}
}
return outObj, nil
}
// objectTemplateBuilders builds ObjectTemplates from ObjectTemplateBuilders
func (n *Inliner) objectTemplateBuilders(ctx context.Context, objects map[string][]*unstructured.Unstructured, ref bundle.ComponentReference) ([]*unstructured.Unstructured, error) {
var outObj []*unstructured.Unstructured
for parentPath, objList := range objects {
for _, obj := range objList {
if obj.GetKind() != "ObjectTemplateBuilder" {
// There shouldn't be any non-ObjectTemplateBuilders at this point
continue
}
name := obj.GetName()
builder := &bundle.ObjectTemplateBuilder{}
if err := converter.FromUnstructured(obj).ToObject(builder); err != nil {
return nil, fmt.Errorf("for component %v and object %q: %v", ref, name, err)
}
parentURL, err := url.Parse(parentPath)
if err != nil {
return nil, err
}
furl, err := builder.File.ParsedURL()
if err != nil {
return nil, err
}
builder.File.URL = makeAbsWithParent(parentURL, furl).String()
contents, err := n.readFile(ctx, builder.File)
if err != nil {
return nil, fmt.Errorf("for component %v and object %q: %v", ref, name, err)
}
objTemplate := &bundle.ObjectTemplate{
TypeMeta: metav1.TypeMeta{
APIVersion: "bundle.gke.io/v1alpha1",
Kind: "ObjectTemplate",
},
ObjectMeta: builder.ObjectMeta,
OptionsSchema: builder.OptionsSchema,
Template: string(contents),
}
objTemplate.ObjectMeta.Annotations = make(map[string]string)
for key, value := range builder.ObjectMeta.Annotations {
objTemplate.ObjectMeta.Annotations[key] = value
}
objTemplate.ObjectMeta.Annotations[string(bundle.InlinePathIdentifier)] = builder.File.URL
tmplType := bundle.TemplateTypeGo
if builder.Type != bundle.TemplateTypeUndefined {
tmplType = builder.Type
}
objTemplate.Type = tmplType
objJSON, err := converter.FromObject(objTemplate).ToJSON()
if err != nil {
return nil, fmt.Errorf("for component %v and object %q, while converting back to JSON: %v", ref, name, err)
}
unsObj, err := converter.FromJSON(objJSON).ToUnstructured()
if err != nil {
return nil, fmt.Errorf("for component %v and object %q, while converting back to Unstructured: %v", ref, name, err)
}
outObj = append(outObj, unsObj)
}
}
return outObj, nil
}
func (n *Inliner) rawTextFiles(ctx context.Context, fileGroups []bundle.FileGroup, ref bundle.ComponentReference, componentPath *url.URL) ([]*unstructured.Unstructured, error) {
var newObjs []*unstructured.Unstructured
for _, fg := range fileGroups {
fgName := fg.Name
if fgName == "" {
return nil, fmt.Errorf("error reading raw text file group object for component %v; name was empty ", ref)
}
m := newConfigMapMaker(fgName)
for _, cf := range fg.Files {
furl, err := cf.ParsedURL()
if err != nil {
return nil, err
}
cf.URL = makeAbsWithParent(componentPath, furl).String()
text, err := n.readFile(ctx, cf)
if err != nil {
return nil, fmt.Errorf("error reading raw text file for component %q: %v", ref, err)
}
dataName := filepath.Base(cf.URL)
if fg.AsBinary {
m.addBinaryData(dataName, text)
} else {
m.addData(dataName, string(text))
}
}
if len(m.cfgMap.Data) > 0 && len(m.cfgMap.BinaryData) > 0 {
return nil, fmt.Errorf("both and binary data were filled out for group: %v", fg)
}
for key, value := range fg.Annotations {
m.cfgMap.ObjectMeta.Annotations[key] = value
}
for key, value := range fg.Labels {
m.cfgMap.ObjectMeta.Labels[key] = value
}
uns, err := m.toUnstructured()
if err != nil {
return nil, fmt.Errorf("for component %v and file group %q, %v", ref, fgName, err)
}
newObjs = append(newObjs, uns)
}
return newObjs, nil
}
// readFile from either a local or remote location.
func (n *Inliner) readFile(ctx context.Context, file bundle.File) ([]byte, error) {
parsed, err := file.ParsedURL()
if err != nil {
return nil, err
}
scheme := files.URLScheme(parsed.Scheme)
if scheme == files.EmptyScheme {
scheme = files.FileScheme
}
rdr, ok := n.Readers[scheme]
if !ok {
return nil, fmt.Errorf("could not find file reader for scheme %q for url %q", parsed.Scheme, file.URL)
}
return rdr.ReadFileObj(ctx, file)
}