github.com/SamarSidharth/kpt@v0.0.0-20231122062228-c7d747ae3ace/commands/alpha/rpkg/update/discover.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  	"fmt"
    19  	"io"
    20  	"strings"
    21  
    22  	porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
    23  	configapi "github.com/GoogleContainerTools/kpt/porch/api/porchconfig/v1alpha1"
    24  	"github.com/spf13/cobra"
    25  	"golang.org/x/mod/semver"
    26  	"k8s.io/cli-runtime/pkg/printers"
    27  	"sigs.k8s.io/controller-runtime/pkg/client"
    28  )
    29  
    30  func (r *runner) discoverUpdates(cmd *cobra.Command, args []string) error {
    31  	var prs []porchapi.PackageRevision
    32  	var errs []string
    33  	if len(args) == 0 || r.discover == downstream {
    34  		prs = r.prs
    35  	} else {
    36  		for i := range args {
    37  			pr := r.findPackageRevision(args[i])
    38  			if pr == nil {
    39  				errs = append(errs, fmt.Sprintf("could not find package revision %s", args[i]))
    40  				continue
    41  			}
    42  			prs = append(prs, *pr)
    43  		}
    44  	}
    45  	if len(errs) > 0 {
    46  		return fmt.Errorf("errors:\n  %s", strings.Join(errs, "\n  "))
    47  	}
    48  
    49  	repositories, err := r.getRepositories()
    50  	if err != nil {
    51  		return err
    52  	}
    53  
    54  	switch r.discover {
    55  	case upstream:
    56  		return r.findUpstreamUpdates(prs, repositories, cmd.OutOrStdout())
    57  	case downstream:
    58  		return r.findDownstreamUpdates(prs, repositories, args, cmd.OutOrStdout())
    59  	default: // this should never happen, because we validate in preRunE
    60  		return fmt.Errorf("invalid argument %q for --discover", r.discover)
    61  	}
    62  }
    63  
    64  func (r *runner) findUpstreamUpdates(prs []porchapi.PackageRevision, repositories *configapi.RepositoryList, w io.Writer) error {
    65  	var upstreamUpdates [][]string
    66  	for _, pr := range prs {
    67  		availableUpdates, upstreamName, _, err := r.availableUpdates(pr.Status.UpstreamLock, repositories)
    68  		if err != nil {
    69  			return fmt.Errorf("could not parse upstreamLock in Kptfile of package %q: %s", pr.Name, err.Error())
    70  		}
    71  		if len(availableUpdates) == 0 {
    72  			upstreamUpdates = append(upstreamUpdates, []string{pr.Name, upstreamName, "No update available"})
    73  		} else {
    74  			var revisions []string
    75  			for i := range availableUpdates {
    76  				revisions = append(revisions, availableUpdates[i].Spec.Revision)
    77  			}
    78  			upstreamUpdates = append(upstreamUpdates, []string{pr.Name, upstreamName, strings.Join(revisions, ", ")})
    79  		}
    80  	}
    81  	return printUpstreamUpdates(upstreamUpdates, w)
    82  }
    83  
    84  func (r *runner) findDownstreamUpdates(prs []porchapi.PackageRevision, repositories *configapi.RepositoryList,
    85  	args []string, w io.Writer) error {
    86  	// map from the upstream package revision to a list of its downstream package revisions
    87  	downstreamUpdatesMap := make(map[string][]porchapi.PackageRevision)
    88  
    89  	for _, pr := range prs {
    90  		availableUpdates, _, draftName, err := r.availableUpdates(pr.Status.UpstreamLock, repositories)
    91  		if err != nil {
    92  			return fmt.Errorf("could not parse upstreamLock in Kptfile of package %q: %s", pr.Name, err.Error())
    93  		}
    94  		for _, update := range availableUpdates {
    95  			key := fmt.Sprintf("%s:%s:%s", update.Name, update.Spec.Revision, draftName)
    96  			downstreamUpdatesMap[key] = append(downstreamUpdatesMap[key], pr)
    97  		}
    98  	}
    99  	return printDownstreamUpdates(downstreamUpdatesMap, args, w)
   100  }
   101  
   102  func (r *runner) availableUpdates(upstreamLock *porchapi.UpstreamLock, repositories *configapi.RepositoryList) ([]porchapi.PackageRevision, string, string, error) {
   103  	var availableUpdates []porchapi.PackageRevision
   104  	var upstream string
   105  
   106  	if upstreamLock == nil || upstreamLock.Git == nil {
   107  		return nil, "", "", nil
   108  	}
   109  	var currentUpstreamRevision string
   110  	var draftName string
   111  
   112  	// separate the revision number from the package name
   113  	lastIndex := strings.LastIndex(upstreamLock.Git.Ref, "/")
   114  	if lastIndex < 0 {
   115  		// "/" not found - upstreamLock.Git.Ref is not in the expected format
   116  		return nil, "", "", fmt.Errorf("malformed upstreamLock.Git.Ref %q", upstreamLock.Git.Ref)
   117  	}
   118  
   119  	if strings.HasPrefix(upstreamLock.Git.Ref, "drafts") {
   120  		// The upstream is not a published package, so doesn't have a revision number.
   121  		// Use v0 as a placeholder, so that all published packages get returned as available
   122  		// updates.
   123  		currentUpstreamRevision = "v0"
   124  		draftName = upstreamLock.Git.Ref[lastIndex+1:]
   125  	} else {
   126  		currentUpstreamRevision = upstreamLock.Git.Ref[lastIndex+1:]
   127  	}
   128  
   129  	// upstream.git.ref could look like drafts/pkgname/version or pkgname/version
   130  	upstreamPackageName := upstreamLock.Git.Ref[:lastIndex]
   131  	upstreamPackageName = strings.TrimPrefix(upstreamPackageName, "drafts")
   132  	upstreamPackageName = strings.TrimPrefix(upstreamPackageName, "/")
   133  
   134  	if !strings.HasSuffix(upstreamLock.Git.Repo, ".git") {
   135  		upstreamLock.Git.Repo += ".git"
   136  	}
   137  
   138  	// find a repo that matches the upstreamLock
   139  	var revisions []porchapi.PackageRevision
   140  	for _, repo := range repositories.Items {
   141  		if repo.Spec.Type != configapi.RepositoryTypeGit {
   142  			// we are not currently supporting non-git repos for updates
   143  			continue
   144  		}
   145  		if !strings.HasSuffix(repo.Spec.Git.Repo, ".git") {
   146  			repo.Spec.Git.Repo += ".git"
   147  		}
   148  		if upstreamLock.Git.Repo == repo.Spec.Git.Repo {
   149  			upstream = repo.Name
   150  			revisions = r.getUpstreamRevisions(repo, upstreamPackageName)
   151  		}
   152  	}
   153  
   154  	for _, upstreamRevision := range revisions {
   155  		switch cmp := semver.Compare(upstreamRevision.Spec.Revision, currentUpstreamRevision); {
   156  		case cmp > 0: // upstreamRevision > currentUpstreamRevision
   157  			availableUpdates = append(availableUpdates, upstreamRevision)
   158  		case cmp == 0, cmp < 0: // upstreamRevision <= currentUpstreamRevision, do nothing
   159  		}
   160  	}
   161  
   162  	return availableUpdates, upstream, draftName, nil
   163  }
   164  
   165  // fetches all registered repositories
   166  func (r *runner) getRepositories() (*configapi.RepositoryList, error) {
   167  	repoList := configapi.RepositoryList{}
   168  	err := r.client.List(r.ctx, &repoList, &client.ListOptions{})
   169  	return &repoList, err
   170  }
   171  
   172  // fetches all package revision numbers for packages with the name upstreamPackageName from the repo
   173  func (r *runner) getUpstreamRevisions(repo configapi.Repository, upstreamPackageName string) []porchapi.PackageRevision {
   174  	var result []porchapi.PackageRevision
   175  	for _, pkgRev := range r.prs {
   176  		if !porchapi.LifecycleIsPublished(pkgRev.Spec.Lifecycle) {
   177  			// only consider published packages
   178  			continue
   179  		}
   180  		if pkgRev.Spec.RepositoryName == repo.Name && pkgRev.Spec.PackageName == upstreamPackageName {
   181  			result = append(result, pkgRev)
   182  		}
   183  	}
   184  	return result
   185  }
   186  
   187  func printUpstreamUpdates(upstreamUpdates [][]string, w io.Writer) error {
   188  	printer := printers.GetNewTabWriter(w)
   189  	if _, err := fmt.Fprintln(printer, "PACKAGE REVISION\tUPSTREAM REPOSITORY\tUPSTREAM UPDATES"); err != nil {
   190  		return err
   191  	}
   192  	for _, pkgRev := range upstreamUpdates {
   193  		if _, err := fmt.Fprintln(printer, strings.Join(pkgRev, "\t")); err != nil {
   194  			return err
   195  		}
   196  	}
   197  	return printer.Flush()
   198  }
   199  
   200  func printDownstreamUpdates(downstreamUpdatesMap map[string][]porchapi.PackageRevision, args []string, w io.Writer) error {
   201  	var downstreamUpdates [][]string
   202  	for upstreamPkgRev, downstreamPkgRevs := range downstreamUpdatesMap {
   203  		split := strings.Split(upstreamPkgRev, ":")
   204  		upstreamPkgRevName := split[0]
   205  		upstreamPkgRevNum := split[1]
   206  		draftName := split[2]
   207  		for _, downstreamPkgRev := range downstreamPkgRevs {
   208  			if draftName != "" {
   209  				// the upstream package revision is not published, so does not have a revision number
   210  				downstreamUpdates = append(downstreamUpdates,
   211  					[]string{upstreamPkgRevName, downstreamPkgRev.Name, fmt.Sprintf("(draft %q)->%s", draftName, upstreamPkgRevNum)})
   212  				continue
   213  			}
   214  			// figure out which upstream revision the downstream revision is based on
   215  			lastIndex := strings.LastIndex(downstreamPkgRev.Status.UpstreamLock.Git.Ref, "v")
   216  			if lastIndex < 0 {
   217  				// this ref isn't formatted the way that porch expects
   218  				continue
   219  			}
   220  			downstreamRev := downstreamPkgRev.Status.UpstreamLock.Git.Ref[lastIndex:]
   221  			downstreamUpdates = append(downstreamUpdates,
   222  				[]string{upstreamPkgRevName, downstreamPkgRev.Name, fmt.Sprintf("%s->%s", downstreamRev, upstreamPkgRevNum)})
   223  		}
   224  	}
   225  
   226  	var pkgRevsToPrint [][]string
   227  	if len(args) != 0 {
   228  		for _, arg := range args {
   229  			for _, pkgRev := range downstreamUpdates {
   230  				// filter out irrelevant packages based on provided args
   231  				if arg == pkgRev[0] {
   232  					pkgRevsToPrint = append(pkgRevsToPrint, pkgRev)
   233  				}
   234  			}
   235  		}
   236  	} else {
   237  		pkgRevsToPrint = downstreamUpdates
   238  	}
   239  
   240  	printer := printers.GetNewTabWriter(w)
   241  	if len(pkgRevsToPrint) == 0 {
   242  		if _, err := fmt.Fprintln(printer, "All downstream packages are up to date."); err != nil {
   243  			return err
   244  		}
   245  	} else {
   246  		if _, err := fmt.Fprintln(printer, "PACKAGE REVISION\tDOWNSTREAM PACKAGE\tDOWNSTREAM UPDATE"); err != nil {
   247  			return err
   248  		}
   249  		for _, pkgRev := range pkgRevsToPrint {
   250  			if _, err := fmt.Fprintln(printer, strings.Join(pkgRev, "\t")); err != nil {
   251  				return err
   252  			}
   253  		}
   254  	}
   255  	return printer.Flush()
   256  }