|
|
|
/*
|
|
|
|
Copyright 2022 The Flux authors
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
http://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 kustomization
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
|
|
|
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
|
|
"sigs.k8s.io/kustomize/api/konfig"
|
|
|
|
"sigs.k8s.io/kustomize/api/krusty"
|
|
|
|
"sigs.k8s.io/kustomize/api/provider"
|
|
|
|
"sigs.k8s.io/kustomize/api/resmap"
|
|
|
|
kustypes "sigs.k8s.io/kustomize/api/types"
|
|
|
|
"sigs.k8s.io/kustomize/kyaml/filesys"
|
|
|
|
"sigs.k8s.io/yaml"
|
|
|
|
|
|
|
|
"github.com/fluxcd/pkg/apis/kustomize"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
specField = "spec"
|
|
|
|
targetNSField = "targetNamespace"
|
|
|
|
patchesField = "patches"
|
|
|
|
patchesSMField = "patchesStrategicMerge"
|
|
|
|
patchesJson6902Field = "patchesJson6902"
|
|
|
|
imagesField = "images"
|
|
|
|
originalKustomizationFile = "kustomization.yaml.original"
|
|
|
|
)
|
|
|
|
|
|
|
|
type action string
|
|
|
|
|
|
|
|
const (
|
|
|
|
createdAction action = "created"
|
|
|
|
unchangedAction action = "unchanged"
|
|
|
|
)
|
|
|
|
|
|
|
|
type KustomizeGenerator struct {
|
|
|
|
kustomization unstructured.Unstructured
|
|
|
|
}
|
|
|
|
|
|
|
|
type SavingOptions func(dirPath, file string, action action) error
|
|
|
|
|
|
|
|
func NewGenerator(kustomization unstructured.Unstructured) *KustomizeGenerator {
|
|
|
|
return &KustomizeGenerator{
|
|
|
|
kustomization: kustomization,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func WithSaveOriginalKustomization() SavingOptions {
|
|
|
|
return func(dirPath, kfile string, action action) error {
|
|
|
|
// copy the original kustomization.yaml to the directory if we did not create it
|
|
|
|
if action != createdAction {
|
|
|
|
if err := copyFile(kfile, filepath.Join(dirPath, originalKustomizationFile)); err != nil {
|
|
|
|
errf := CleanDirectory(dirPath, action)
|
|
|
|
return fmt.Errorf("%v %v", err, errf)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// WriteFile generates a kustomization.yaml in the given directory if it does not exist.
|
|
|
|
// It apply the flux kustomize resources to the kustomization.yaml and then write the
|
|
|
|
// updated kustomization.yaml to the directory.
|
|
|
|
// It returns an action that indicates if the kustomization.yaml was created or not.
|
|
|
|
// It is the caller responsability to clean up the directory by use the provided function CleanDirectory.
|
|
|
|
// example:
|
|
|
|
// err := CleanDirectory(dirPath, action)
|
|
|
|
// if err != nil {
|
|
|
|
// log.Fatal(err)
|
|
|
|
// }
|
|
|
|
func (kg *KustomizeGenerator) WriteFile(dirPath string, opts ...SavingOptions) (action, error) {
|
|
|
|
action, err := kg.generateKustomization(dirPath)
|
|
|
|
if err != nil {
|
|
|
|
errf := CleanDirectory(dirPath, action)
|
|
|
|
return action, fmt.Errorf("%v %v", err, errf)
|
|
|
|
}
|
|
|
|
|
|
|
|
kfile := filepath.Join(dirPath, konfig.DefaultKustomizationFileName())
|
|
|
|
|
|
|
|
data, err := os.ReadFile(kfile)
|
|
|
|
if err != nil {
|
|
|
|
errf := CleanDirectory(dirPath, action)
|
|
|
|
return action, fmt.Errorf("%w %s", err, errf)
|
|
|
|
}
|
|
|
|
|
|
|
|
kus := kustypes.Kustomization{
|
|
|
|
TypeMeta: kustypes.TypeMeta{
|
|
|
|
APIVersion: kustypes.KustomizationVersion,
|
|
|
|
Kind: kustypes.KustomizationKind,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := yaml.Unmarshal(data, &kus); err != nil {
|
|
|
|
errf := CleanDirectory(dirPath, action)
|
|
|
|
return action, fmt.Errorf("%v %v", err, errf)
|
|
|
|
}
|
|
|
|
|
|
|
|
tg, ok, err := kg.getNestedString(specField, targetNSField)
|
|
|
|
if err != nil {
|
|
|
|
errf := CleanDirectory(dirPath, action)
|
|
|
|
return action, fmt.Errorf("%v %v", err, errf)
|
|
|
|
}
|
|
|
|
if ok {
|
|
|
|
kus.Namespace = tg
|
|
|
|
}
|
|
|
|
|
|
|
|
patches, err := kg.getPatches()
|
|
|
|
if err != nil {
|
|
|
|
errf := CleanDirectory(dirPath, action)
|
|
|
|
return action, fmt.Errorf("unable to get patches: %w", fmt.Errorf("%v %v", err, errf))
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, p := range patches {
|
|
|
|
kus.Patches = append(kus.Patches, kustypes.Patch{
|
|
|
|
Patch: p.Patch,
|
|
|
|
Target: adaptSelector(&p.Target),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
patchesSM, err := kg.getPatchesStrategicMerge()
|
|
|
|
if err != nil {
|
|
|
|
errf := CleanDirectory(dirPath, action)
|
|
|
|
return action, fmt.Errorf("unable to get patchesStrategicMerge: %w", fmt.Errorf("%v %v", err, errf))
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, p := range patchesSM {
|
|
|
|
kus.PatchesStrategicMerge = append(kus.PatchesStrategicMerge, kustypes.PatchStrategicMerge(p.Raw))
|
|
|
|
}
|
|
|
|
|
|
|
|
patchesJSON, err := kg.getPatchesJson6902()
|
|
|
|
if err != nil {
|
|
|
|
errf := CleanDirectory(dirPath, action)
|
|
|
|
return action, fmt.Errorf("unable to get patchesJson6902: %w", fmt.Errorf("%v %v", err, errf))
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, p := range patchesJSON {
|
|
|
|
patch, err := json.Marshal(p.Patch)
|
|
|
|
if err != nil {
|
|
|
|
errf := CleanDirectory(dirPath, action)
|
|
|
|
return action, fmt.Errorf("%v %v", err, errf)
|
|
|
|
}
|
|
|
|
kus.PatchesJson6902 = append(kus.PatchesJson6902, kustypes.Patch{
|
|
|
|
Patch: string(patch),
|
|
|
|
Target: adaptSelector(&p.Target),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
images, err := kg.getImages()
|
|
|
|
if err != nil {
|
|
|
|
errf := CleanDirectory(dirPath, action)
|
|
|
|
return action, fmt.Errorf("unable to get images: %w", fmt.Errorf("%v %v", err, errf))
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, image := range images {
|
|
|
|
newImage := kustypes.Image{
|
|
|
|
Name: image.Name,
|
|
|
|
NewName: image.NewName,
|
|
|
|
NewTag: image.NewTag,
|
|
|
|
}
|
|
|
|
if exists, index := checkKustomizeImageExists(kus.Images, image.Name); exists {
|
|
|
|
kus.Images[index] = newImage
|
|
|
|
} else {
|
|
|
|
kus.Images = append(kus.Images, newImage)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
manifest, err := yaml.Marshal(kus)
|
|
|
|
if err != nil {
|
|
|
|
errf := CleanDirectory(dirPath, action)
|
|
|
|
return action, fmt.Errorf("%v %v", err, errf)
|
|
|
|
}
|
|
|
|
|
|
|
|
// copy the original kustomization.yaml to the directory if we did not create it
|
|
|
|
for _, opt := range opts {
|
|
|
|
if err := opt(dirPath, kfile, action); err != nil {
|
|
|
|
return action, fmt.Errorf("failed to save original kustomization.yaml: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
err = os.WriteFile(kfile, manifest, os.ModePerm)
|
|
|
|
if err != nil {
|
|
|
|
errf := CleanDirectory(dirPath, action)
|
|
|
|
return action, fmt.Errorf("%v %v", err, errf)
|
|
|
|
}
|
|
|
|
|
|
|
|
return action, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (kg *KustomizeGenerator) getPatches() ([]kustomize.Patch, error) {
|
|
|
|
patches, ok, err := kg.getNestedSlice(specField, patchesField)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var resultErr error
|
|
|
|
if ok {
|
|
|
|
res := make([]kustomize.Patch, 0, len(patches))
|
|
|
|
for k, p := range patches {
|
|
|
|
patch, ok := p.(map[string]interface{})
|
|
|
|
if !ok {
|
|
|
|
err := fmt.Errorf("unable to convert patch %d to map[string]interface{}", k)
|
|
|
|
resultErr = multierror.Append(resultErr, err)
|
|
|
|
}
|
|
|
|
var kpatch kustomize.Patch
|
|
|
|
err = runtime.DefaultUnstructuredConverter.FromUnstructured(patch, &kpatch)
|
|
|
|
if err != nil {
|
|
|
|
resultErr = multierror.Append(resultErr, err)
|
|
|
|
}
|
|
|
|
res = append(res, kpatch)
|
|
|
|
}
|
|
|
|
return res, resultErr
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, resultErr
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func (kg *KustomizeGenerator) getPatchesStrategicMerge() ([]apiextensionsv1.JSON, error) {
|
|
|
|
patches, ok, err := kg.getNestedSlice(specField, patchesSMField)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var resultErr error
|
|
|
|
if ok {
|
|
|
|
res := make([]apiextensionsv1.JSON, 0, len(patches))
|
|
|
|
for k, p := range patches {
|
|
|
|
patch, ok := p.(map[string]interface{})
|
|
|
|
if !ok {
|
|
|
|
err := fmt.Errorf("unable to convert patch %d to map[string]interface{}", k)
|
|
|
|
resultErr = multierror.Append(resultErr, err)
|
|
|
|
}
|
|
|
|
var kpatch apiextensionsv1.JSON
|
|
|
|
err = runtime.DefaultUnstructuredConverter.FromUnstructured(patch, &kpatch)
|
|
|
|
if err != nil {
|
|
|
|
resultErr = multierror.Append(resultErr, err)
|
|
|
|
}
|
|
|
|
res = append(res, kpatch)
|
|
|
|
}
|
|
|
|
return res, resultErr
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, resultErr
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func (kg *KustomizeGenerator) getPatchesJson6902() ([]kustomize.JSON6902Patch, error) {
|
|
|
|
patches, ok, err := kg.getNestedSlice(specField, patchesJson6902Field)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var resultErr error
|
|
|
|
if ok {
|
|
|
|
res := make([]kustomize.JSON6902Patch, 0, len(patches))
|
|
|
|
for k, p := range patches {
|
|
|
|
patch, ok := p.(map[string]interface{})
|
|
|
|
if !ok {
|
|
|
|
err := fmt.Errorf("unable to convert patch %d to map[string]interface{}", k)
|
|
|
|
resultErr = multierror.Append(resultErr, err)
|
|
|
|
}
|
|
|
|
var kpatch kustomize.JSON6902Patch
|
|
|
|
err = runtime.DefaultUnstructuredConverter.FromUnstructured(patch, &kpatch)
|
|
|
|
if err != nil {
|
|
|
|
resultErr = multierror.Append(resultErr, err)
|
|
|
|
}
|
|
|
|
res = append(res, kpatch)
|
|
|
|
}
|
|
|
|
return res, resultErr
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, resultErr
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func (kg *KustomizeGenerator) getImages() ([]kustomize.Image, error) {
|
|
|
|
img, ok, err := kg.getNestedSlice(specField, imagesField)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var resultErr error
|
|
|
|
if ok {
|
|
|
|
res := make([]kustomize.Image, 0, len(img))
|
|
|
|
for k, i := range img {
|
|
|
|
im, ok := i.(map[string]interface{})
|
|
|
|
if !ok {
|
|
|
|
err := fmt.Errorf("unable to convert patch %d to map[string]interface{}", k)
|
|
|
|
resultErr = multierror.Append(resultErr, err)
|
|
|
|
}
|
|
|
|
var image kustomize.Image
|
|
|
|
err = runtime.DefaultUnstructuredConverter.FromUnstructured(im, &image)
|
|
|
|
if err != nil {
|
|
|
|
resultErr = multierror.Append(resultErr, err)
|
|
|
|
}
|
|
|
|
res = append(res, image)
|
|
|
|
}
|
|
|
|
return res, resultErr
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, resultErr
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func checkKustomizeImageExists(images []kustypes.Image, imageName string) (bool, int) {
|
|
|
|
for i, image := range images {
|
|
|
|
if imageName == image.Name {
|
|
|
|
return true, i
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false, -1
|
|
|
|
}
|
|
|
|
|
|
|
|
func (kg *KustomizeGenerator) getNestedString(fields ...string) (string, bool, error) {
|
|
|
|
val, ok, err := unstructured.NestedString(kg.kustomization.Object, fields...)
|
|
|
|
if err != nil {
|
|
|
|
return "", ok, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return val, ok, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (kg *KustomizeGenerator) getNestedSlice(fields ...string) ([]interface{}, bool, error) {
|
|
|
|
val, ok, err := unstructured.NestedSlice(kg.kustomization.Object, fields...)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ok, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return val, ok, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (kg *KustomizeGenerator) generateKustomization(dirPath string) (action, error) {
|
|
|
|
fs := filesys.MakeFsOnDisk()
|
|
|
|
|
|
|
|
// Determine if there already is a Kustomization file at the root,
|
|
|
|
// as this means we do not have to generate one.
|
|
|
|
for _, kfilename := range konfig.RecognizedKustomizationFileNames() {
|
|
|
|
if kpath := filepath.Join(dirPath, kfilename); fs.Exists(kpath) && !fs.IsDir(kpath) {
|
|
|
|
return unchangedAction, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
scan := func(base string) ([]string, error) {
|
|
|
|
var paths []string
|
|
|
|
pvd := provider.NewDefaultDepProvider()
|
|
|
|
rf := pvd.GetResourceFactory()
|
|
|
|
err := fs.Walk(base, func(path string, info os.FileInfo, err error) error {
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if path == base {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if info.IsDir() {
|
|
|
|
// If a sub-directory contains an existing kustomization file add the
|
|
|
|
// directory as a resource and do not decend into it.
|
|
|
|
for _, kfilename := range konfig.RecognizedKustomizationFileNames() {
|
|
|
|
if kpath := filepath.Join(path, kfilename); fs.Exists(kpath) && !fs.IsDir(kpath) {
|
|
|
|
paths = append(paths, path)
|
|
|
|
return filepath.SkipDir
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
extension := filepath.Ext(path)
|
|
|
|
if extension != ".yaml" && extension != ".yml" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
fContents, err := fs.ReadFile(path)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := rf.SliceFromBytes(fContents); err != nil {
|
|
|
|
return fmt.Errorf("failed to decode Kubernetes YAML from %s: %w", path, err)
|
|
|
|
}
|
|
|
|
paths = append(paths, path)
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
return paths, err
|
|
|
|
}
|
|
|
|
|
|
|
|
abs, err := filepath.Abs(dirPath)
|
|
|
|
if err != nil {
|
|
|
|
return unchangedAction, err
|
|
|
|
}
|
|
|
|
|
|
|
|
files, err := scan(abs)
|
|
|
|
if err != nil {
|
|
|
|
return unchangedAction, err
|
|
|
|
}
|
|
|
|
|
|
|
|
kfile := filepath.Join(dirPath, konfig.DefaultKustomizationFileName())
|
|
|
|
f, err := fs.Create(kfile)
|
|
|
|
if err != nil {
|
|
|
|
return unchangedAction, err
|
|
|
|
}
|
|
|
|
f.Close()
|
|
|
|
|
|
|
|
kus := kustypes.Kustomization{
|
|
|
|
TypeMeta: kustypes.TypeMeta{
|
|
|
|
APIVersion: kustypes.KustomizationVersion,
|
|
|
|
Kind: kustypes.KustomizationKind,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
var resources []string
|
|
|
|
for _, file := range files {
|
|
|
|
resources = append(resources, strings.Replace(file, abs, ".", 1))
|
|
|
|
}
|
|
|
|
|
|
|
|
kus.Resources = resources
|
|
|
|
kd, err := yaml.Marshal(kus)
|
|
|
|
if err != nil {
|
|
|
|
// delete the kustomization file
|
|
|
|
errf := CleanDirectory(dirPath, createdAction)
|
|
|
|
return unchangedAction, fmt.Errorf("%v %v", err, errf)
|
|
|
|
}
|
|
|
|
|
|
|
|
return createdAction, os.WriteFile(kfile, kd, os.ModePerm)
|
|
|
|
}
|
|
|
|
|
|
|
|
func adaptSelector(selector *kustomize.Selector) (output *kustypes.Selector) {
|
|
|
|
if selector != nil {
|
|
|
|
output = &kustypes.Selector{}
|
|
|
|
output.Gvk.Group = selector.Group
|
|
|
|
output.Gvk.Kind = selector.Kind
|
|
|
|
output.Gvk.Version = selector.Version
|
|
|
|
output.Name = selector.Name
|
|
|
|
output.Namespace = selector.Namespace
|
|
|
|
output.LabelSelector = selector.LabelSelector
|
|
|
|
output.AnnotationSelector = selector.AnnotationSelector
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: remove mutex when kustomize fixes the concurrent map read/write panic
|
|
|
|
var kustomizeBuildMutex sync.Mutex
|
|
|
|
|
|
|
|
// BuildKustomization wraps krusty.MakeKustomizer with the following settings:
|
|
|
|
// - load files from outside the kustomization.yaml root
|
|
|
|
// - disable plugins except for the builtin ones
|
|
|
|
func BuildKustomization(fs filesys.FileSystem, dirPath string) (resmap.ResMap, error) {
|
|
|
|
// temporary workaround for concurrent map read and map write bug
|
|
|
|
// https://github.com/kubernetes-sigs/kustomize/issues/3659
|
|
|
|
kustomizeBuildMutex.Lock()
|
|
|
|
defer kustomizeBuildMutex.Unlock()
|
|
|
|
|
|
|
|
buildOptions := &krusty.Options{
|
|
|
|
LoadRestrictions: kustypes.LoadRestrictionsNone,
|
|
|
|
PluginConfig: kustypes.DisabledPluginConfig(),
|
|
|
|
}
|
|
|
|
|
|
|
|
k := krusty.MakeKustomizer(buildOptions)
|
|
|
|
return k.Run(fs, dirPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
// CleanDirectory removes the kustomization.yaml file from the given directory.
|
|
|
|
func CleanDirectory(dirPath string, action action) error {
|
|
|
|
kfile := filepath.Join(dirPath, konfig.DefaultKustomizationFileName())
|
|
|
|
originalFile := filepath.Join(dirPath, originalKustomizationFile)
|
|
|
|
|
|
|
|
// restore old file if it exists
|
|
|
|
if _, err := os.Stat(originalFile); err == nil {
|
|
|
|
err := os.Rename(originalFile, kfile)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to cleanup repository: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if action == createdAction {
|
|
|
|
return os.Remove(kfile)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// copyFile copies the contents of the file named src to the file named
|
|
|
|
// by dst. The file will be created if it does not already exist or else trucnated.
|
|
|
|
func copyFile(src, dst string) (err error) {
|
|
|
|
in, err := os.Open(src)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
defer in.Close()
|
|
|
|
|
|
|
|
out, err := os.Create(dst)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
errf := out.Close()
|
|
|
|
if err == nil {
|
|
|
|
err = errf
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
if _, err = io.Copy(out, in); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|