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 }