get.porter.sh/porter@v1.3.0/pkg/porter/apply.go (about)

     1  package porter
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  
     8  	"get.porter.sh/porter/pkg/encoding"
     9  	"get.porter.sh/porter/pkg/portercontext"
    10  	"get.porter.sh/porter/pkg/printer"
    11  	"get.porter.sh/porter/pkg/storage"
    12  	"get.porter.sh/porter/pkg/tracing"
    13  	"go.opentelemetry.io/otel/attribute"
    14  	"go.uber.org/zap/zapcore"
    15  )
    16  
    17  type ApplyOptions struct {
    18  	Namespace string
    19  	File      string
    20  
    21  	// Force the installation to be re-applied regardless of anything being changed or not
    22  	Force bool
    23  
    24  	// DryRun only checks if the changes would trigger a bundle run
    25  	DryRun bool
    26  }
    27  
    28  const ApplyDefaultFormat = printer.FormatPlaintext
    29  
    30  var ApplyAllowedFormats = printer.Formats{printer.FormatPlaintext, printer.FormatYaml, printer.FormatJson}
    31  
    32  func (o *ApplyOptions) Validate(cxt *portercontext.Context, args []string) error {
    33  	switch len(args) {
    34  	case 0:
    35  		return errors.New("a file argument is required")
    36  	case 1:
    37  		o.File = args[0]
    38  	default:
    39  		return errors.New("only one file argument may be specified")
    40  	}
    41  
    42  	info, err := cxt.FileSystem.Stat(o.File)
    43  	if err != nil {
    44  		return fmt.Errorf("invalid file argument %s: %w", o.File, err)
    45  	}
    46  	if info.IsDir() {
    47  		return fmt.Errorf("invalid file argument %s, must be a file not a directory", o.File)
    48  	}
    49  
    50  	return nil
    51  }
    52  
    53  func (p *Porter) InstallationApply(ctx context.Context, opts ApplyOptions) error {
    54  	ctx, log := tracing.StartSpan(ctx)
    55  	defer log.EndSpan()
    56  
    57  	log.Debugf("Reading input file %s", opts.File)
    58  
    59  	namespace, err := p.getNamespaceFromFile(opts)
    60  	if err != nil {
    61  		return log.Error(err)
    62  	}
    63  
    64  	if log.ShouldLog(zapcore.DebugLevel) {
    65  		// ignoring any error here, printing debug info isn't critical
    66  		contents, _ := p.FileSystem.ReadFile(opts.File)
    67  		log.Debug("read input file", attribute.String("contents", string(contents)))
    68  	}
    69  
    70  	var input DisplayInstallation
    71  	if err := encoding.UnmarshalFile(p.FileSystem, opts.File, &input); err != nil {
    72  		return log.Errorf("unable to parse %s as an installation document: %w", opts.File, err)
    73  	}
    74  	input.Namespace = namespace
    75  	inputInstallation, err := input.ConvertToInstallation()
    76  	if err != nil {
    77  		return log.Error(err)
    78  	}
    79  
    80  	installation, err := p.Installations.GetInstallation(ctx, inputInstallation.Namespace, inputInstallation.Name)
    81  	if err != nil {
    82  		if !errors.Is(err, storage.ErrNotFound{}) {
    83  			return log.Errorf("could not query for an existing installation document for %s: %w", inputInstallation, err)
    84  		}
    85  
    86  		// Create a new installation
    87  		installation = storage.NewInstallation(input.Namespace, input.Name)
    88  		log.Info("Creating a new installation", attribute.String("installation", installation.String()))
    89  	} else {
    90  		log.Infof("Updating %s installation\n", installation)
    91  	}
    92  
    93  	installation.Apply(inputInstallation.InstallationSpec)
    94  	checkStrategy := p.GetSchemaCheckStrategy(ctx)
    95  	if err := installation.Validate(ctx, checkStrategy); err != nil {
    96  		return log.Errorf("invalid installation: %w", err)
    97  	}
    98  
    99  	reconcileOpts := ReconcileOptions{
   100  		Namespace:    input.Namespace,
   101  		Name:         input.Name,
   102  		Installation: installation,
   103  		Force:        opts.Force,
   104  		DryRun:       opts.DryRun,
   105  	}
   106  	return p.ReconcileInstallation(ctx, reconcileOpts)
   107  }