pkg/commands/cmdlib/bundleio.go (176 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 cmdlib
import (
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
log "k8s.io/klog"
bundle "github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/apis/bundle/v1alpha1"
"github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/build"
"github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/converter"
"github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/files"
"github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/options"
"github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/wrapper"
)
type makeInliner func(rw files.FileReaderWriter, inputFile string) fileInliner
func realInlinerMaker(rw files.FileReaderWriter, inputFile string) fileInliner {
return build.NewInlinerWithScheme(
files.FileScheme,
&files.LocalFileObjReader{
WorkingDir: filepath.Dir(inputFile),
Rdr: rw,
})
}
// StdioReaderWriter can read from STDIN and write to STDOUT.
type StdioReaderWriter interface {
ReadAll() ([]byte, error)
io.Writer
}
// RealStdioReaderWriter provides a real STDIN / STDOUT implementation.
type RealStdioReaderWriter struct{}
// ReadAll reads all the input from STDIN.
func (r *RealStdioReaderWriter) ReadAll() ([]byte, error) {
return ioutil.ReadAll(os.Stdin)
}
// Write writes content to STDOUT.
func (r *RealStdioReaderWriter) Write(b []byte) (int, error) {
return os.Stdout.Write(b)
}
type fileInliner interface {
BundleFiles(context.Context, *bundle.BundleBuilder, string) (*bundle.Bundle, error)
ComponentFiles(context.Context, *bundle.ComponentBuilder, string) (*bundle.Component, error)
}
// BundleReaderWriter provides a mockable reading / writing interface for
// reading / writing bundle data.
type BundleReaderWriter interface {
ReadBundleData(context.Context, *GlobalOptions) (*wrapper.BundleWrapper, error)
WriteBundleData(context.Context, *wrapper.BundleWrapper, *GlobalOptions) error
WriteStructuredContents(context.Context, interface{}, *GlobalOptions) error
}
// BundleReaderWriter is an object that can read and write bundle information,
// in the context of CLI flags.
type realBundleReaderWriter struct {
rw files.FileReaderWriter
stdio StdioReaderWriter
makeInlinerFn makeInliner
}
// NewBundleReaderWriter creates a new BundleReaderWriter.
func NewBundleReaderWriter(rw files.FileReaderWriter, stdio StdioReaderWriter) BundleReaderWriter {
return &realBundleReaderWriter{
rw: rw,
stdio: stdio,
makeInlinerFn: realInlinerMaker,
}
}
// ReadBundleData reads either data file contents from a file or stdin.
func (brw *realBundleReaderWriter) ReadBundleData(ctx context.Context, g *GlobalOptions) (*wrapper.BundleWrapper, error) {
var bytes []byte
var err error
inFmt := g.InputFormat
if g.InputFile != "" {
log.V(4).Infof("Reading input file %v", g.InputFile)
bytes, err = brw.rw.ReadFile(ctx, g.InputFile)
if err != nil {
return nil, err
}
} else {
log.V(4).Info("No component data file, reading from stdin")
if bytes, err = brw.stdio.ReadAll(); err != nil {
return nil, err
}
}
fileFmt := formatFromFile(g.InputFile)
if fileFmt != "" && inFmt != "" {
inFmt = fileFmt
}
if inFmt == "" {
inFmt = "yaml"
}
bw, err := wrapper.FromRaw(inFmt, bytes)
if err != nil {
return nil, err
}
// For now, we can only inline component data files because we need the path
// context.
if g.InputFile != "" && (bw.BundleBuilder() != nil || bw.ComponentBuilder() != nil) {
return brw.inlineData(ctx, bw, g)
}
return bw, nil
}
// inlineData inlines a cluster bundle before processing
func (brw *realBundleReaderWriter) inlineData(ctx context.Context, bw *wrapper.BundleWrapper, g *GlobalOptions) (*wrapper.BundleWrapper, error) {
infile := g.InputFile
inliner := brw.makeInlinerFn(brw.rw, infile)
switch bw.Kind() {
case "BundleBuilder":
newBun, err := inliner.BundleFiles(ctx, bw.BundleBuilder(), infile)
if err != nil {
return nil, fmt.Errorf("inlining component data files: %v", err)
}
return wrapper.FromBundle(newBun), nil
case "ComponentBuilder":
newComp, err := inliner.ComponentFiles(ctx, bw.ComponentBuilder(), infile)
if err != nil {
return nil, fmt.Errorf("inlining objects: %v", err)
}
return wrapper.FromComponent(newComp), nil
default:
// Bundle and Component types can't be inlined.
return bw, nil
}
}
// WriteBundleData writes either the component or bundle object from the BundleWrapper.
func (brw *realBundleReaderWriter) WriteBundleData(ctx context.Context, bw *wrapper.BundleWrapper, g *GlobalOptions) error {
if bw == nil {
return fmt.Errorf("bundle wrapper was nil")
}
obj := bw.Object()
if obj == nil {
return fmt.Errorf("wrapped bundle object was nil")
}
return brw.WriteStructuredContents(ctx, obj, g)
}
// WriteStructuredContents writes some structured contents from some object
// `obj`. The contents must be serializable to both JSON and YAML.
func (brw *realBundleReaderWriter) WriteStructuredContents(ctx context.Context, obj interface{}, g *GlobalOptions) error {
outFmt := g.OutputFormat
if outFmt == "" {
outFmt = "yaml"
}
bytes, err := converter.FromObject(obj).ToContentType(outFmt)
if err != nil {
return fmt.Errorf("error writing contents: %v", err)
}
return brw.writeContents(ctx, bytes, brw.rw)
}
// writeContents writes some bytes to stdout. if outPath is empty, write to
func (brw *realBundleReaderWriter) writeContents(ctx context.Context, bytes []byte, rw files.FileReaderWriter) error {
_, err := brw.stdio.Write(bytes)
return err
}
// formatFromFile gets the content format from a file-extension and returns
// empty string if the extension couldn't be mapped
func formatFromFile(path string) string {
ext := filepath.Ext(path)
switch ext {
case ".yaml", ".yml":
return "yaml"
case ".json":
return "json"
}
return ""
}
// ParseStringMap parses a CLI flag value into a map of string to string. It
// expects the raw flag value to have the form "key1=value1,key2=value2,etc".
func ParseStringMap(p string) map[string]string {
if p == "" {
return nil
}
m := make(map[string]string)
splat := strings.Split(p, ",")
for _, v := range splat {
kv := strings.Split(v, "=")
if len(kv) == 2 {
m[kv[0]] = kv[1]
}
}
return m
}
// MergeOptions reads multiple options files and combines them into a single map.
// Option values will be overwritten if a later file has the same key.
func MergeOptions(ctx context.Context, rw files.FileReaderWriter, files []string) (options.JSONOptions, error) {
optData := make(map[string]interface{})
for _, f := range files {
bytes, err := rw.ReadFile(ctx, f)
if err != nil {
return nil, err
}
data, err := converter.FromFileName(f, bytes).ToJSONMap()
if err != nil {
return nil, err
}
for k, v := range data {
optData[k] = v
}
}
return optData, nil
}