github.com/SamarSidharth/kpt@v0.0.0-20231122062228-c7d747ae3ace/commands/alpha/rpkg/clone/command.go (about)

     1  // Copyright 2022 The kpt Authors
     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/commands/alpha/rpkg/util"
    23  	"github.com/GoogleContainerTools/kpt/internal/docs/generated/rpkgdocs"
    24  	"github.com/GoogleContainerTools/kpt/internal/errors"
    25  	"github.com/GoogleContainerTools/kpt/internal/util/parse"
    26  	"github.com/GoogleContainerTools/kpt/internal/util/porch"
    27  	porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
    28  	"github.com/spf13/cobra"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/cli-runtime/pkg/genericclioptions"
    31  	"sigs.k8s.io/controller-runtime/pkg/client"
    32  )
    33  
    34  const (
    35  	command = "cmdrpkgclone"
    36  )
    37  
    38  var (
    39  	strategies = []string{
    40  		string(porchapi.ResourceMerge),
    41  		string(porchapi.FastForward),
    42  		string(porchapi.ForceDeleteReplace),
    43  	}
    44  )
    45  
    46  func NewCommand(ctx context.Context, rcg *genericclioptions.ConfigFlags) *cobra.Command {
    47  	return newRunner(ctx, rcg).Command
    48  }
    49  
    50  func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner {
    51  	r := &runner{
    52  		ctx: ctx,
    53  		cfg: rcg,
    54  	}
    55  	c := &cobra.Command{
    56  		Use:     "clone SOURCE_PACKAGE NAME",
    57  		Short:   rpkgdocs.CloneShort,
    58  		Long:    rpkgdocs.CloneShort + "\n" + rpkgdocs.CloneLong,
    59  		Example: rpkgdocs.CloneExamples,
    60  		PreRunE: r.preRunE,
    61  		RunE:    r.runE,
    62  		Hidden:  porch.HidePorchCommands,
    63  	}
    64  	r.Command = c
    65  
    66  	c.Flags().StringVar(&r.strategy, "strategy", string(porchapi.ResourceMerge),
    67  		"update strategy that should be used when updating this package; one of: "+strings.Join(strategies, ","))
    68  	c.Flags().StringVar(&r.directory, "directory", "", "Directory within the repository where the upstream package is located.")
    69  	c.Flags().StringVar(&r.ref, "ref", "", "Branch in the repository where the upstream package is located.")
    70  	c.Flags().StringVar(&r.repository, "repository", "", "Repository to which package will be cloned (downstream repository).")
    71  	c.Flags().StringVar(&r.workspace, "workspace", "v1", "Workspace name of the downstream package.")
    72  
    73  	return r
    74  }
    75  
    76  type runner struct {
    77  	ctx     context.Context
    78  	cfg     *genericclioptions.ConfigFlags
    79  	client  client.Client
    80  	Command *cobra.Command
    81  
    82  	clone porchapi.PackageCloneTaskSpec
    83  
    84  	// Flags
    85  	strategy   string
    86  	directory  string
    87  	ref        string
    88  	repository string // Target repository
    89  	workspace  string // Target workspaceName
    90  	target     string // Target package name
    91  }
    92  
    93  func (r *runner) preRunE(_ *cobra.Command, args []string) error {
    94  	const op errors.Op = command + ".preRunE"
    95  	client, err := porch.CreateClientWithFlags(r.cfg)
    96  	if err != nil {
    97  		return errors.E(op, err)
    98  	}
    99  	r.client = client
   100  
   101  	mergeStrategy, err := toMergeStrategy(r.strategy)
   102  	if err != nil {
   103  		return errors.E(op, err)
   104  	}
   105  	r.clone.Strategy = mergeStrategy
   106  
   107  	if len(args) < 2 {
   108  		return errors.E(op, fmt.Errorf("SOURCE_PACKAGE and NAME are required positional arguments; %d provided", len(args)))
   109  	}
   110  
   111  	if r.repository == "" {
   112  		return errors.E(op, fmt.Errorf("--repository is required to specify downstream repository"))
   113  	}
   114  
   115  	if r.workspace == "" {
   116  		return errors.E(op, fmt.Errorf("--workspace is required to specify downstream workspace name"))
   117  	}
   118  
   119  	source := args[0]
   120  	target := args[1]
   121  
   122  	pkgExists, err := util.PackageAlreadyExists(r.ctx, r.client, r.repository, target, *r.cfg.Namespace)
   123  	if err != nil {
   124  		return err
   125  	}
   126  	if pkgExists {
   127  		return fmt.Errorf("`clone` cannot create a new revision for package %q that already exists in repo %q; make subsequent revisions using `copy`",
   128  			target, r.repository)
   129  	}
   130  
   131  	switch {
   132  	case strings.HasPrefix(source, "oci://"):
   133  		r.clone.Upstream.Type = porchapi.RepositoryTypeOCI
   134  		r.clone.Upstream.Oci = &porchapi.OciPackage{
   135  			Image: source,
   136  		}
   137  
   138  	case strings.Contains(source, "/"):
   139  		if parse.HasGitSuffix(source) { // extra parsing required
   140  			repo, dir, ref, err := parse.URL(source)
   141  			if err != nil {
   142  				return err
   143  			}
   144  			// throw error if values set by flags contradict values parsed from SOURCE_PACKAGE
   145  			if r.directory != "" && dir != "" && r.directory != dir {
   146  				return errors.E(op, fmt.Errorf("directory %s specified by --directory contradicts directory %s specified by SOURCE_PACKAGE",
   147  					r.directory, dir))
   148  			}
   149  			if r.ref != "" && ref != "" && r.ref != ref {
   150  				return errors.E(op, fmt.Errorf("ref %s specified by --ref contradicts ref %s specified by SOURCE_PACKAGE",
   151  					r.ref, ref))
   152  			}
   153  			// grab the values parsed from SOURCE_PACKAGE
   154  			if r.directory == "" {
   155  				r.directory = dir
   156  			}
   157  			if r.ref == "" {
   158  				r.ref = ref
   159  			}
   160  			source = repo + ".git" // parse.ParseURL removes the git suffix, we need to add it back
   161  		}
   162  		if r.ref == "" {
   163  			r.ref = "main"
   164  		}
   165  		if r.directory == "" {
   166  			r.directory = "/"
   167  		}
   168  		r.clone.Upstream.Type = porchapi.RepositoryTypeGit
   169  		r.clone.Upstream.Git = &porchapi.GitPackage{
   170  			Repo:      source,
   171  			Ref:       r.ref,
   172  			Directory: r.directory,
   173  		}
   174  		// TODO: support authn
   175  
   176  	default:
   177  		r.clone.Upstream.UpstreamRef = &porchapi.PackageRevisionRef{
   178  			Name: source,
   179  		}
   180  	}
   181  
   182  	r.target = target
   183  	return nil
   184  }
   185  
   186  func (r *runner) runE(cmd *cobra.Command, _ []string) error {
   187  	const op errors.Op = command + ".runE"
   188  
   189  	pr := &porchapi.PackageRevision{
   190  		TypeMeta: metav1.TypeMeta{
   191  			Kind:       "PackageRevision",
   192  			APIVersion: porchapi.SchemeGroupVersion.Identifier(),
   193  		},
   194  		ObjectMeta: metav1.ObjectMeta{
   195  			Namespace: *r.cfg.Namespace,
   196  		},
   197  		Spec: porchapi.PackageRevisionSpec{
   198  			PackageName:    r.target,
   199  			WorkspaceName:  porchapi.WorkspaceName(r.workspace),
   200  			RepositoryName: r.repository,
   201  			Tasks: []porchapi.Task{
   202  				{
   203  					Type:  porchapi.TaskTypeClone,
   204  					Clone: &r.clone,
   205  				},
   206  			},
   207  		},
   208  	}
   209  	if err := r.client.Create(r.ctx, pr); err != nil {
   210  		return errors.E(op, err)
   211  	}
   212  
   213  	fmt.Fprintf(cmd.OutOrStdout(), "%s created\n", pr.Name)
   214  	return nil
   215  }
   216  
   217  func toMergeStrategy(strategy string) (porchapi.PackageMergeStrategy, error) {
   218  	switch strategy {
   219  	case string(porchapi.ResourceMerge):
   220  		return porchapi.ResourceMerge, nil
   221  	case string(porchapi.FastForward):
   222  		return porchapi.FastForward, nil
   223  	case string(porchapi.ForceDeleteReplace):
   224  		return porchapi.ForceDeleteReplace, nil
   225  	default:
   226  		return "", fmt.Errorf("invalid strategy: %q", strategy)
   227  	}
   228  }