github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/commands/alpha/rpkg/update/discover.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 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 := r.availableUpdates(pr.Status.UpstreamLock, repositories)
    68  		if len(availableUpdates) == 0 {
    69  			upstreamUpdates = append(upstreamUpdates, []string{pr.Name, upstreamName, "No update available"})
    70  		} else {
    71  			var revisions []string
    72  			for i := range availableUpdates {
    73  				revisions = append(revisions, availableUpdates[i].Spec.Revision)
    74  			}
    75  			upstreamUpdates = append(upstreamUpdates, []string{pr.Name, upstreamName, strings.Join(revisions, ", ")})
    76  		}
    77  	}
    78  	return printUpstreamUpdates(upstreamUpdates, w)
    79  }
    80  
    81  func (r *runner) findDownstreamUpdates(prs []porchapi.PackageRevision, repositories *configapi.RepositoryList,
    82  	args []string, w io.Writer) error {
    83  	// map from the upstream package revision to a list of its downstream package revisions
    84  	downstreamUpdatesMap := make(map[string][]porchapi.PackageRevision)
    85  
    86  	for _, pr := range prs {
    87  		availableUpdates, _ := r.availableUpdates(pr.Status.UpstreamLock, repositories)
    88  		for _, update := range availableUpdates {
    89  			key := fmt.Sprintf("%s:%s", update.Name, update.Spec.Revision)
    90  			downstreamUpdatesMap[key] = append(downstreamUpdatesMap[key], pr)
    91  		}
    92  	}
    93  	return printDownstreamUpdates(downstreamUpdatesMap, args, w)
    94  }
    95  
    96  func (r *runner) availableUpdates(upstreamLock *porchapi.UpstreamLock, repositories *configapi.RepositoryList) ([]porchapi.PackageRevision, string) {
    97  	var availableUpdates []porchapi.PackageRevision
    98  	var upstream string
    99  
   100  	if upstreamLock == nil || upstreamLock.Git == nil {
   101  		return nil, ""
   102  	}
   103  	// separate the revision number from the package name
   104  	lastIndex := strings.LastIndex(upstreamLock.Git.Ref, "v")
   105  	if lastIndex < 0 {
   106  		return nil, ""
   107  	}
   108  	currentUpstreamRevision := upstreamLock.Git.Ref[lastIndex:]
   109  
   110  	// upstream.git.ref could look like drafts/pkgname/version or pkgname/version
   111  	upstreamPackageName := upstreamLock.Git.Ref[:lastIndex-1]
   112  	upstreamPackageName = strings.TrimPrefix(upstreamPackageName, "drafts/")
   113  
   114  	if !strings.HasSuffix(upstreamLock.Git.Repo, ".git") {
   115  		upstreamLock.Git.Repo += ".git"
   116  	}
   117  
   118  	// find a repo that matches the upstreamLock
   119  	var revisions []porchapi.PackageRevision
   120  	for _, repo := range repositories.Items {
   121  		if repo.Spec.Type != configapi.RepositoryTypeGit {
   122  			// we are not currently supporting non-git repos for updates
   123  			continue
   124  		}
   125  		if !strings.HasSuffix(repo.Spec.Git.Repo, ".git") {
   126  			repo.Spec.Git.Repo += ".git"
   127  		}
   128  		if upstreamLock.Git.Repo == repo.Spec.Git.Repo {
   129  			upstream = repo.Name
   130  			revisions = r.getUpstreamRevisions(repo, upstreamPackageName)
   131  		}
   132  	}
   133  
   134  	for _, upstreamRevision := range revisions {
   135  		switch cmp := semver.Compare(upstreamRevision.Spec.Revision, currentUpstreamRevision); {
   136  		case cmp > 0: // upstreamRevision > currentUpstreamRevision
   137  			availableUpdates = append(availableUpdates, upstreamRevision)
   138  		case cmp == 0, cmp < 0: // upstreamRevision <= currentUpstreamRevision, do nothing
   139  		}
   140  	}
   141  
   142  	return availableUpdates, upstream
   143  }
   144  
   145  // fetches all registered repositories
   146  func (r *runner) getRepositories() (*configapi.RepositoryList, error) {
   147  	repoList := configapi.RepositoryList{}
   148  	err := r.client.List(r.ctx, &repoList, &client.ListOptions{})
   149  	return &repoList, err
   150  }
   151  
   152  // fetches all package revision numbers for packages with the name upstreamPackageName from the repo
   153  func (r *runner) getUpstreamRevisions(repo configapi.Repository, upstreamPackageName string) []porchapi.PackageRevision {
   154  	var result []porchapi.PackageRevision
   155  	for _, pkgRev := range r.prs {
   156  		if pkgRev.Spec.Lifecycle != porchapi.PackageRevisionLifecyclePublished {
   157  			// only consider published packages
   158  			continue
   159  		}
   160  		if pkgRev.Spec.RepositoryName == repo.Name && pkgRev.Spec.PackageName == upstreamPackageName {
   161  			result = append(result, pkgRev)
   162  		}
   163  	}
   164  	return result
   165  }
   166  
   167  func printUpstreamUpdates(upstreamUpdates [][]string, w io.Writer) error {
   168  	printer := printers.GetNewTabWriter(w)
   169  	if _, err := fmt.Fprintln(printer, "PACKAGE REVISION\tUPSTREAM REPOSITORY\tUPSTREAM UPDATES"); err != nil {
   170  		return err
   171  	}
   172  	for _, pkgRev := range upstreamUpdates {
   173  		if _, err := fmt.Fprintln(printer, strings.Join(pkgRev, "\t")); err != nil {
   174  			return err
   175  		}
   176  	}
   177  	return printer.Flush()
   178  }
   179  
   180  func printDownstreamUpdates(downstreamUpdatesMap map[string][]porchapi.PackageRevision, args []string, w io.Writer) error {
   181  	var downstreamUpdates [][]string
   182  	for upstreamPkgRev, downstreamPkgRevs := range downstreamUpdatesMap {
   183  		split := strings.Split(upstreamPkgRev, ":")
   184  		upstreamPkgRevName := split[0]
   185  		upstreamPkgRevNum := split[1]
   186  		for _, downstreamPkgRev := range downstreamPkgRevs {
   187  			// figure out which upstream revision the downstream revision is based on
   188  			lastIndex := strings.LastIndex(downstreamPkgRev.Status.UpstreamLock.Git.Ref, "v")
   189  			if lastIndex < 0 {
   190  				// this ref isn't formatted the way that porch expects
   191  				continue
   192  			}
   193  			downstreamRev := downstreamPkgRev.Status.UpstreamLock.Git.Ref[lastIndex:]
   194  			downstreamUpdates = append(downstreamUpdates,
   195  				[]string{upstreamPkgRevName, downstreamPkgRev.Name, fmt.Sprintf("%s->%s", downstreamRev, upstreamPkgRevNum)})
   196  		}
   197  	}
   198  
   199  	var pkgRevsToPrint [][]string
   200  	if len(args) != 0 {
   201  		for _, arg := range args {
   202  			for _, pkgRev := range downstreamUpdates {
   203  				// filter out irrelevant packages based on provided args
   204  				if arg == pkgRev[0] {
   205  					pkgRevsToPrint = append(pkgRevsToPrint, pkgRev)
   206  				}
   207  			}
   208  		}
   209  	} else {
   210  		pkgRevsToPrint = downstreamUpdates
   211  	}
   212  
   213  	printer := printers.GetNewTabWriter(w)
   214  	if len(pkgRevsToPrint) == 0 {
   215  		if _, err := fmt.Fprintln(printer, "All downstream packages are up to date."); err != nil {
   216  			return err
   217  		}
   218  	} else {
   219  		if _, err := fmt.Fprintln(printer, "PACKAGE REVISION\tDOWNSTREAM PACKAGE\tDOWNSTREAM UPDATE"); err != nil {
   220  			return err
   221  		}
   222  		for _, pkgRev := range pkgRevsToPrint {
   223  			if _, err := fmt.Fprintln(printer, strings.Join(pkgRev, "\t")); err != nil {
   224  				return err
   225  			}
   226  		}
   227  	}
   228  	return printer.Flush()
   229  }