github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/commands/alpha/rpkg/clone/command.go (about)

     1  // Copyright 2022 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package clone
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"strings"
    21  
    22  	"github.com/GoogleContainerTools/kpt/internal/docs/generated/rpkgdocs"
    23  	"github.com/GoogleContainerTools/kpt/internal/errors"
    24  	"github.com/GoogleContainerTools/kpt/internal/util/parse"
    25  	"github.com/GoogleContainerTools/kpt/internal/util/porch"
    26  	porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
    27  	"github.com/spf13/cobra"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/cli-runtime/pkg/genericclioptions"
    30  	"sigs.k8s.io/controller-runtime/pkg/client"
    31  )
    32  
    33  const (
    34  	command = "cmdrpkgclone"
    35  )
    36  
    37  var (
    38  	strategies = []string{
    39  		string(porchapi.ResourceMerge),
    40  		string(porchapi.FastForward),
    41  		string(porchapi.ForceDeleteReplace),
    42  	}
    43  )
    44  
    45  func NewCommand(ctx context.Context, rcg *genericclioptions.ConfigFlags) *cobra.Command {
    46  	return newRunner(ctx, rcg).Command
    47  }
    48  
    49  func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner {
    50  	r := &runner{
    51  		ctx: ctx,
    52  		cfg: rcg,
    53  	}
    54  	c := &cobra.Command{
    55  		Use:     "clone SOURCE_PACKAGE NAME",
    56  		Short:   rpkgdocs.CloneShort,
    57  		Long:    rpkgdocs.CloneShort + "\n" + rpkgdocs.CloneLong,
    58  		Example: rpkgdocs.CloneExamples,
    59  		PreRunE: r.preRunE,
    60  		RunE:    r.runE,
    61  		Hidden:  porch.HidePorchCommands,
    62  	}
    63  	r.Command = c
    64  
    65  	c.Flags().StringVar(&r.strategy, "strategy", string(porchapi.ResourceMerge),
    66  		"update strategy that should be used when updating this package; one of: "+strings.Join(strategies, ","))
    67  	c.Flags().StringVar(&r.directory, "directory", "", "Directory within the repository where the upstream package is located.")
    68  	c.Flags().StringVar(&r.ref, "ref", "", "Branch in the repository where the upstream package is located.")
    69  	c.Flags().StringVar(&r.repository, "repository", "", "Repository to which package will be cloned (downstream repository).")
    70  	c.Flags().StringVar(&r.revision, "revision", "v1", "Revision of the downstream package.")
    71  
    72  	return r
    73  }
    74  
    75  type runner struct {
    76  	ctx     context.Context
    77  	cfg     *genericclioptions.ConfigFlags
    78  	client  client.Client
    79  	Command *cobra.Command
    80  
    81  	clone porchapi.PackageCloneTaskSpec
    82  
    83  	// Flags
    84  	strategy   string
    85  	directory  string
    86  	ref        string
    87  	repository string // Target repository
    88  	revision   string // Target package revision
    89  	target     string // Target package name
    90  }
    91  
    92  func (r *runner) preRunE(cmd *cobra.Command, args []string) error {
    93  	const op errors.Op = command + ".preRunE"
    94  	client, err := porch.CreateClient(r.cfg)
    95  	if err != nil {
    96  		return errors.E(op, err)
    97  	}
    98  	r.client = client
    99  
   100  	mergeStrategy, err := toMergeStrategy(r.strategy)
   101  	if err != nil {
   102  		return errors.E(op, err)
   103  	}
   104  	r.clone.Strategy = mergeStrategy
   105  
   106  	if len(args) < 2 {
   107  		return errors.E(op, fmt.Errorf("SOURCE_PACKAGE and NAME are required positional arguments; %d provided", len(args)))
   108  	}
   109  
   110  	if r.repository == "" {
   111  		return errors.E(op, fmt.Errorf("--repository is required to specify downstream repository"))
   112  	}
   113  
   114  	source := args[0]
   115  	target := args[1]
   116  
   117  	switch {
   118  	case strings.HasPrefix(source, "oci://"):
   119  		r.clone.Upstream.Type = porchapi.RepositoryTypeOCI
   120  		r.clone.Upstream.Oci = &porchapi.OciPackage{
   121  			Image: source,
   122  		}
   123  
   124  	case strings.Contains(source, "/"):
   125  		if parse.HasGitSuffix(source) { // extra parsing required
   126  			repo, dir, ref, err := parse.URL(source)
   127  			if err != nil {
   128  				return err
   129  			}
   130  			// throw error if values set by flags contradict values parsed from SOURCE_PACKAGE
   131  			if r.directory != "" && dir != "" && r.directory != dir {
   132  				return errors.E(op, fmt.Errorf("directory %s specified by --directory contradicts directory %s specified by SOURCE_PACKAGE",
   133  					r.directory, dir))
   134  			}
   135  			if r.ref != "" && ref != "" && r.ref != ref {
   136  				return errors.E(op, fmt.Errorf("ref %s specified by --ref contradicts ref %s specified by SOURCE_PACKAGE",
   137  					r.ref, ref))
   138  			}
   139  			// grab the values parsed from SOURCE_PACKAGE
   140  			if r.directory == "" {
   141  				r.directory = dir
   142  			}
   143  			if r.ref == "" {
   144  				r.ref = ref
   145  			}
   146  			source = repo + ".git" // parse.ParseURL removes the git suffix, we need to add it back
   147  		}
   148  		if r.ref == "" {
   149  			r.ref = "main"
   150  		}
   151  		if r.directory == "" {
   152  			r.directory = "/"
   153  		}
   154  		r.clone.Upstream.Type = porchapi.RepositoryTypeGit
   155  		r.clone.Upstream.Git = &porchapi.GitPackage{
   156  			Repo:      source,
   157  			Ref:       r.ref,
   158  			Directory: r.directory,
   159  		}
   160  		// TODO: support authn
   161  
   162  	default:
   163  		r.clone.Upstream.UpstreamRef = &porchapi.PackageRevisionRef{
   164  			Name: source,
   165  		}
   166  	}
   167  
   168  	r.target = target
   169  	return nil
   170  }
   171  
   172  func (r *runner) runE(cmd *cobra.Command, args []string) error {
   173  	const op errors.Op = command + ".runE"
   174  
   175  	pr := &porchapi.PackageRevision{
   176  		TypeMeta: metav1.TypeMeta{
   177  			Kind:       "PackageRevision",
   178  			APIVersion: porchapi.SchemeGroupVersion.Identifier(),
   179  		},
   180  		ObjectMeta: metav1.ObjectMeta{
   181  			Namespace: *r.cfg.Namespace,
   182  		},
   183  		Spec: porchapi.PackageRevisionSpec{
   184  			PackageName:    r.target,
   185  			Revision:       r.revision,
   186  			RepositoryName: r.repository,
   187  			Tasks: []porchapi.Task{
   188  				{
   189  					Type:  porchapi.TaskTypeClone,
   190  					Clone: &r.clone,
   191  				},
   192  			},
   193  		},
   194  	}
   195  	if err := r.client.Create(r.ctx, pr); err != nil {
   196  		return errors.E(op, err)
   197  	}
   198  
   199  	fmt.Fprintf(cmd.OutOrStdout(), "%s created\n", pr.Name)
   200  	return nil
   201  }
   202  
   203  func toMergeStrategy(strategy string) (porchapi.PackageMergeStrategy, error) {
   204  	switch strategy {
   205  	case string(porchapi.ResourceMerge):
   206  		return porchapi.ResourceMerge, nil
   207  	case string(porchapi.FastForward):
   208  		return porchapi.FastForward, nil
   209  	case string(porchapi.ForceDeleteReplace):
   210  		return porchapi.ForceDeleteReplace, nil
   211  	default:
   212  		return "", fmt.Errorf("invalid strategy: %q", strategy)
   213  	}
   214  }