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

Allow saving imported resources to separate YAML files #2963

Merged
merged 7 commits into from
May 13, 2023
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
78 changes: 61 additions & 17 deletions v2/cmd/asoctl/cmd/import_azure_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,37 @@ import (
)

func newImportAzureResourceCommand() *cobra.Command {
var outputPath *string
var options importAzureResourceOptions

cmd := &cobra.Command{
Use: "azure-resource <ARM/ID/of/resource>",
Short: "Import ARM resources as Custom Resources",
Args: cobra.ArbitraryArgs,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
return importAzureResource(ctx, args, outputPath)
return importAzureResource(ctx, args, options)
},
}

outputPath = cmd.Flags().StringP(
options.outputPath = cmd.Flags().StringP(
"output",
"o",
"",
"Write ARM resource CRD to a file")
"Write ARM resource CRDs to a single file")

options.outputFolder = cmd.Flags().StringP(
"output-folder",
"f",
"",
"Write ARM resource CRDs to individual files in a folder")

cmd.MarkFlagsMutuallyExclusive("output", "output-folder")

return cmd
}

// importAzureResource imports an ARM resource and writes the YAML to stdout or a file
func importAzureResource(ctx context.Context, armIDs []string, outputPath *string) error {
func importAzureResource(ctx context.Context, armIDs []string, options importAzureResourceOptions) error {

log, progress := CreateLoggerAndProgressBar()

Expand All @@ -55,11 +63,11 @@ func importAzureResource(ctx context.Context, armIDs []string, outputPath *strin
return errors.Wrap(err, "unable to get default Azure credential")
}

options := &genericarmclient.GenericClientOptions{
clientOptions := &genericarmclient.GenericClientOptions{
UserAgent: "asoctl/" + version.BuildVersion,
}

client, err := genericarmclient.NewGenericClient(activeCloud, creds, options)
client, err := genericarmclient.NewGenericClient(activeCloud, creds, clientOptions)
if err != nil {
return errors.Wrapf(err, "failed to create ARM client")
}
Expand All @@ -75,26 +83,62 @@ func importAzureResource(ctx context.Context, armIDs []string, outputPath *strin
result, err := importer.Import(ctx)

// Wait for progress bar to finish & flush
progress.Wait()
defer func() {
progress.Wait()
}()

if err != nil {
return errors.Wrap(err, "failed to import resources")
}

if outputPath == nil || *outputPath == "" {
err := result.SaveToWriter(os.Stdout)
if result.Count() == 0 {
log.Info("No resources found, nothing to save.")
return nil
}

if file, ok := options.writeToFile(); ok {
log.Info(
"Writing to a single file",
"file", file)
err := result.SaveToSingleFile(file)
if err != nil {
return errors.Wrapf(err, "failed to write to stdout")
return errors.Wrapf(err, "failed to write to file %s", file)
}
} else {
} else if folder, ok := options.writeToFolder(); ok {
log.Info(
"Writing to file",
"path", *outputPath)
err := result.SaveToFile(*outputPath)
"Writing to individual files in folder",
"folder", folder)
err := result.SaveToIndividualFilesInFolder(folder)
if err != nil {
return errors.Wrapf(err, "failed to write into folder %s", folder)
}
} else {
err := result.SaveToWriter(os.Stdout)
if err != nil {
return errors.Wrapf(err, "failed to write to file %s", *outputPath)
return errors.Wrapf(err, "failed to write to stdout")
}
}

return nil
}

type importAzureResourceOptions struct {
outputPath *string
outputFolder *string
}

func (option *importAzureResourceOptions) writeToFile() (string, bool) {
if option.outputPath != nil && *option.outputPath != "" {
return *option.outputPath, true
}

return "", false
}

func (option *importAzureResourceOptions) writeToFolder() (string, bool) {
if option.outputFolder != nil && *option.outputFolder != "" {
return *option.outputFolder, true
}

return "", false
}
97 changes: 68 additions & 29 deletions v2/cmd/asoctl/internal/importing/resource_import_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ package importing

import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"

"github.com/pkg/errors"
"golang.org/x/exp/slices"
Expand All @@ -22,7 +24,71 @@ type ResourceImportResult struct {
resources []genruntime.MetaObject
}

// Count returns the number of successfully imported resources.
func (r *ResourceImportResult) Count() int {
return len(r.resources)
}

func (r *ResourceImportResult) SaveToWriter(destination io.Writer) error {
return r.writeTo(r.resources, destination)
}

func (r *ResourceImportResult) SaveToSingleFile(filepath string) error {
return r.saveTo(r.resources, filepath)
}

func (r *ResourceImportResult) SaveToIndividualFilesInFolder(folder string) error {
// We name the files after the resource type and name
// We allocate resources to files using a map, just in case we have a naming collision
// (If that happens, all the similarly named resources will be in the same file, which is not ideal,
// but better than dropping one or more)
fileMap := make(map[string][]genruntime.MetaObject, len(r.resources))
for _, resource := range r.resources {
resourceName := resource.GetName()
typeName := resource.GetObjectKind().GroupVersionKind().Kind
theunrepentantgeek marked this conversation as resolved.
Show resolved Hide resolved
fileName := fmt.Sprintf("%s-%s.yaml", typeName, resourceName)
fileMap[fileName] = append(fileMap[fileName], resource)
}

for fileName, resources := range fileMap {
path := filepath.Join(folder, fileName)
err := r.saveTo(resources, path)
if err != nil {
return errors.Wrapf(err, "unable to save to file %s", path)
}
}

return nil
}

func (r *ResourceImportResult) saveTo(resources []genruntime.MetaObject, path string) error {
file, err := os.Create(path)
if err != nil {
return errors.Wrapf(err, "unable to create file %s", path)
}

defer func() {
file.Close()

// if we are panicking, the file will be in a broken
// state, so remove it
if r := recover(); r != nil {
os.Remove(path)
panic(r)
}
}()

err = r.writeTo(resources, file)
if err != nil {
// cleanup in case of errors
file.Close()
os.Remove(path)
}

return errors.Wrapf(err, "unable to save to file %s", path)
}

func (*ResourceImportResult) writeTo(resources []genruntime.MetaObject, destination io.Writer) error {
buf := bufio.NewWriter(destination)
defer func(buf *bufio.Writer) {
_ = buf.Flush()
Expand All @@ -34,7 +100,7 @@ func (r *ResourceImportResult) SaveToWriter(destination io.Writer) error {
}

// Sort objects into a deterministic order
slices.SortFunc(r.resources, func(left genruntime.MetaObject, right genruntime.MetaObject) bool {
slices.SortFunc(resources, func(left genruntime.MetaObject, right genruntime.MetaObject) bool {
leftGVK := left.GetObjectKind().GroupVersionKind()
rightGVK := right.GetObjectKind().GroupVersionKind()

Expand All @@ -53,7 +119,7 @@ func (r *ResourceImportResult) SaveToWriter(destination io.Writer) error {
return left.GetName() < right.GetName()
})

for _, resource := range r.resources {
for _, resource := range resources {
data, err := yaml.Marshal(resource)
if err != nil {
return errors.Wrap(err, "unable to save to writer")
Expand All @@ -72,30 +138,3 @@ func (r *ResourceImportResult) SaveToWriter(destination io.Writer) error {

return nil
}

func (r *ResourceImportResult) SaveToFile(filepath string) error {
file, err := os.Create(filepath)
if err != nil {
return errors.Wrapf(err, "unable to create file %s", filepath)
}

defer func() {
file.Close()

// if we are panicking, the file will be in a broken
// state, so remove it
if r := recover(); r != nil {
os.Remove(filepath)
panic(r)
}
}()

err = r.SaveToWriter(file)
if err != nil {
// cleanup in case of errors
file.Close()
os.Remove(filepath)
}

return errors.Wrapf(err, "unable to save to file %s", filepath)
}