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 }