github.com/SamarSidharth/kpt@v0.0.0-20231122062228-c7d747ae3ace/commands/alpha/rpkg/update/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 update
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  
    21  	"github.com/GoogleContainerTools/kpt/internal/docs/generated/rpkgdocs"
    22  	"github.com/GoogleContainerTools/kpt/internal/errors"
    23  	"github.com/GoogleContainerTools/kpt/internal/util/porch"
    24  	porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
    25  	"github.com/spf13/cobra"
    26  	"k8s.io/cli-runtime/pkg/genericclioptions"
    27  	"sigs.k8s.io/controller-runtime/pkg/client"
    28  )
    29  
    30  const (
    31  	command = "cmdrpkgupdate"
    32  
    33  	upstream   = "upstream"
    34  	downstream = "downstream"
    35  )
    36  
    37  func NewCommand(ctx context.Context, rcg *genericclioptions.ConfigFlags) *cobra.Command {
    38  	return newRunner(ctx, rcg).Command
    39  }
    40  
    41  func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner {
    42  	r := &runner{
    43  		ctx: ctx,
    44  		cfg: rcg,
    45  	}
    46  	r.Command = &cobra.Command{
    47  		Use:     "update SOURCE_PACKAGE",
    48  		PreRunE: r.preRunE,
    49  		RunE:    r.runE,
    50  		Short:   rpkgdocs.UpdateShort,
    51  		Long:    rpkgdocs.UpdateShort + "\n" + rpkgdocs.UpdateLong,
    52  		Example: rpkgdocs.UpdateExamples,
    53  		Hidden:  porch.HidePorchCommands,
    54  	}
    55  	r.Command.Flags().StringVar(&r.revision, "revision", "", "Revision of the upstream package to update to.")
    56  	r.Command.Flags().StringVar(&r.discover, "discover", "",
    57  		`If set, search for available updates instead of performing an update. 
    58  Setting this to 'upstream' will discover upstream updates of downstream packages.
    59  Setting this to 'downstream' will discover downstream package revisions of upstream packages that need to be updated.`)
    60  	return r
    61  }
    62  
    63  type runner struct {
    64  	ctx     context.Context
    65  	cfg     *genericclioptions.ConfigFlags
    66  	client  client.Client
    67  	Command *cobra.Command
    68  
    69  	revision string // Target package revision
    70  	discover string // If set, discover updates rather than do updates
    71  
    72  	// there are multiple places where we need access to all package revisions, so
    73  	// we store it in the runner
    74  	prs []porchapi.PackageRevision
    75  }
    76  
    77  func (r *runner) preRunE(_ *cobra.Command, args []string) error {
    78  	const op errors.Op = command + ".preRunE"
    79  	c, err := porch.CreateClientWithFlags(r.cfg)
    80  	if err != nil {
    81  		return errors.E(op, err)
    82  	}
    83  	r.client = c
    84  
    85  	if r.discover == "" {
    86  		if len(args) < 1 {
    87  			return errors.E(op, fmt.Errorf("SOURCE_PACKAGE is a required positional argument"))
    88  		}
    89  		if len(args) > 1 {
    90  			return errors.E(op, fmt.Errorf("too many arguments; SOURCE_PACKAGE is the only accepted positional arguments"))
    91  		}
    92  		// TODO: This should use the latest available revision if one isn't specified.
    93  		if r.revision == "" {
    94  			return errors.E(op, fmt.Errorf("revision is required"))
    95  		}
    96  	} else if r.discover != upstream && r.discover != downstream {
    97  		return errors.E(op, fmt.Errorf("argument for 'discover' must be one of 'upstream' or 'downstream'"))
    98  	}
    99  
   100  	packageRevisionList := porchapi.PackageRevisionList{}
   101  	if err := r.client.List(r.ctx, &packageRevisionList, &client.ListOptions{}); err != nil {
   102  		return errors.E(op, err)
   103  	}
   104  	r.prs = packageRevisionList.Items
   105  
   106  	return nil
   107  }
   108  
   109  func (r *runner) runE(cmd *cobra.Command, args []string) error {
   110  	const op errors.Op = command + ".runE"
   111  
   112  	if r.discover == "" {
   113  		pr := r.findPackageRevision(args[0])
   114  		if pr == nil {
   115  			return errors.E(op, fmt.Errorf("could not find package revision %s", args[0]))
   116  		}
   117  		if err := r.doUpdate(pr); err != nil {
   118  			return errors.E(op, err)
   119  		}
   120  		if _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s updated\n", pr.Name); err != nil {
   121  			return errors.E(op, err)
   122  		}
   123  	} else if err := r.discoverUpdates(cmd, args); err != nil {
   124  		return errors.E(op, err)
   125  	}
   126  	return nil
   127  }
   128  
   129  func (r *runner) doUpdate(pr *porchapi.PackageRevision) error {
   130  	cloneTask := r.findCloneTask(pr)
   131  	if cloneTask == nil {
   132  		return fmt.Errorf("upstream source not found for package rev %q; only cloned packages can be updated", pr.Spec.PackageName)
   133  	}
   134  
   135  	switch cloneTask.Clone.Upstream.Type {
   136  	case porchapi.RepositoryTypeGit:
   137  		cloneTask.Clone.Upstream.Git.Ref = r.revision
   138  	case porchapi.RepositoryTypeOCI:
   139  		return fmt.Errorf("update not implemented for oci packages")
   140  	default:
   141  		upstreamPr := r.findPackageRevision(cloneTask.Clone.Upstream.UpstreamRef.Name)
   142  		if upstreamPr == nil {
   143  			return fmt.Errorf("upstream package revision %s no longer exists", cloneTask.Clone.Upstream.UpstreamRef.Name)
   144  		}
   145  		newUpstreamPr := r.findPackageRevisionForRef(upstreamPr.Spec.PackageName)
   146  		if newUpstreamPr == nil {
   147  			return fmt.Errorf("revision %s does not exist for package %s", r.revision, pr.Spec.PackageName)
   148  		}
   149  		newTask := porchapi.Task{
   150  			Type: porchapi.TaskTypeUpdate,
   151  			Update: &porchapi.PackageUpdateTaskSpec{
   152  				Upstream: cloneTask.Clone.Upstream,
   153  			},
   154  		}
   155  		newTask.Update.Upstream.UpstreamRef.Name = newUpstreamPr.Name
   156  		pr.Spec.Tasks = append(pr.Spec.Tasks, newTask)
   157  	}
   158  
   159  	return r.client.Update(r.ctx, pr)
   160  }
   161  
   162  func (r *runner) findPackageRevision(prName string) *porchapi.PackageRevision {
   163  	for i := range r.prs {
   164  		pr := r.prs[i]
   165  		if pr.Name == prName {
   166  			return &pr
   167  		}
   168  	}
   169  	return nil
   170  }
   171  
   172  func (r *runner) findCloneTask(pr *porchapi.PackageRevision) *porchapi.Task {
   173  	if len(pr.Spec.Tasks) == 0 {
   174  		return nil
   175  	}
   176  	firstTask := pr.Spec.Tasks[0]
   177  	if firstTask.Type == porchapi.TaskTypeClone {
   178  		return &firstTask
   179  	}
   180  	return nil
   181  }
   182  
   183  func (r *runner) findPackageRevisionForRef(name string) *porchapi.PackageRevision {
   184  	for i := range r.prs {
   185  		pr := r.prs[i]
   186  		if pr.Spec.PackageName == name && pr.Spec.Revision == r.revision {
   187  			return &pr
   188  		}
   189  	}
   190  	return nil
   191  }