github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/cd-service/pkg/service/git.go (about) 1 /*This file is part of kuberpult. 2 3 Kuberpult is free software: you can redistribute it and/or modify 4 it under the terms of the Expat(MIT) License as published by 5 the Free Software Foundation. 6 7 Kuberpult is distributed in the hope that it will be useful, 8 but WITHOUT ANY WARRANTY; without even the implied warranty of 9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 MIT License for more details. 11 12 You should have received a copy of the MIT License 13 along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>. 14 15 Copyright 2023 freiheit.com*/ 16 17 package service 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "os" 24 "sort" 25 "strconv" 26 "strings" 27 28 api "github.com/freiheit-com/kuberpult/pkg/api/v1" 29 grpcErrors "github.com/freiheit-com/kuberpult/pkg/grpc" 30 "github.com/freiheit-com/kuberpult/pkg/valid" 31 eventmod "github.com/freiheit-com/kuberpult/services/cd-service/pkg/event" 32 "github.com/freiheit-com/kuberpult/services/cd-service/pkg/repository" 33 billy "github.com/go-git/go-billy/v5" 34 "github.com/go-git/go-billy/v5/util" 35 "github.com/onokonem/sillyQueueServer/timeuuid" 36 "google.golang.org/grpc/codes" 37 "google.golang.org/grpc/status" 38 ) 39 40 type GitServer struct { 41 Config repository.RepositoryConfig 42 OverviewService *OverviewServiceServer 43 } 44 45 func (s *GitServer) GetGitTags(ctx context.Context, in *api.GetGitTagsRequest) (*api.GetGitTagsResponse, error) { 46 tags, err := repository.GetTags(s.Config, "./repository_tags", ctx) 47 if err != nil { 48 return nil, fmt.Errorf("unable to get tags from repository: %v", err) 49 } 50 51 return &api.GetGitTagsResponse{TagData: tags}, nil 52 } 53 54 func (s *GitServer) GetProductSummary(ctx context.Context, in *api.GetProductSummaryRequest) (*api.GetProductSummaryResponse, error) { 55 if in.Environment == nil && in.EnvironmentGroup == nil { 56 return nil, fmt.Errorf("Must have an environment or environmentGroup to get the product summary for") 57 } 58 if in.Environment != nil && in.EnvironmentGroup != nil { 59 if *in.Environment != "" && *in.EnvironmentGroup != "" { 60 return nil, fmt.Errorf("Can not have both an environment and environmentGroup to get the product summary for") 61 } 62 } 63 if in.CommitHash == "" { 64 return nil, fmt.Errorf("Must have a commit to get the product summary for") 65 } 66 response, err := s.OverviewService.GetOverview(ctx, &api.GetOverviewRequest{GitRevision: in.CommitHash}) 67 if err != nil { 68 return nil, fmt.Errorf("unable to get overview for %s: %v", in.CommitHash, err) 69 } 70 71 var summaryFromEnv []api.ProductSummary 72 if in.Environment != nil && *in.Environment != "" { 73 for _, group := range response.EnvironmentGroups { 74 for _, env := range group.Environments { 75 if env.Name == *in.Environment { 76 for _, app := range env.Applications { 77 summaryFromEnv = append(summaryFromEnv, api.ProductSummary{ 78 CommitId: "", 79 DisplayVersion: "", 80 Team: "", 81 App: app.Name, 82 Version: strconv.FormatUint(app.Version, 10), 83 Environment: *in.Environment, 84 }) 85 } 86 } 87 } 88 } 89 if len(summaryFromEnv) == 0 { 90 return &api.GetProductSummaryResponse{ 91 ProductSummary: nil, 92 }, nil 93 } 94 sort.Slice(summaryFromEnv, func(i, j int) bool { 95 a := summaryFromEnv[i].App 96 b := summaryFromEnv[j].App 97 return a < b 98 }) 99 } else { 100 for _, group := range response.EnvironmentGroups { 101 if *in.EnvironmentGroup == group.EnvironmentGroupName { 102 for _, env := range group.Environments { 103 var singleEnvSummary []api.ProductSummary 104 for _, app := range env.Applications { 105 singleEnvSummary = append(singleEnvSummary, api.ProductSummary{ 106 CommitId: "", 107 DisplayVersion: "", 108 Team: "", 109 App: app.Name, 110 Version: strconv.FormatUint(app.Version, 10), 111 Environment: env.Name, 112 }) 113 } 114 sort.Slice(singleEnvSummary, func(i, j int) bool { 115 a := singleEnvSummary[i].App 116 b := singleEnvSummary[j].App 117 return a < b 118 }) 119 summaryFromEnv = append(summaryFromEnv, singleEnvSummary...) 120 } 121 } 122 } 123 if len(summaryFromEnv) == 0 { 124 return nil, nil 125 } 126 } 127 128 var productVersion []*api.ProductSummary 129 for _, row := range summaryFromEnv { //nolint: govet 130 for _, app := range response.Applications { 131 if row.App == app.Name { 132 for _, release := range app.Releases { 133 if strconv.FormatUint(release.Version, 10) == row.Version { 134 productVersion = append(productVersion, &api.ProductSummary{App: row.App, Version: row.Version, CommitId: release.SourceCommitId, DisplayVersion: release.DisplayVersion, Environment: row.Environment, Team: app.Team}) 135 break 136 } 137 } 138 } 139 } 140 } 141 return &api.GetProductSummaryResponse{ProductSummary: productVersion}, nil 142 } 143 144 func (s *GitServer) GetCommitInfo(ctx context.Context, in *api.GetCommitInfoRequest) (*api.GetCommitInfoResponse, error) { 145 if !s.Config.WriteCommitData { 146 return nil, status.Error(codes.FailedPrecondition, "no written commit info available; set KUBERPULT_GIT_WRITE_COMMIT_DATA=true to enable") 147 } 148 149 fs := s.OverviewService.Repository.State().Filesystem 150 151 commitIDPrefix := in.CommitHash 152 153 commitID, err := findCommitID(ctx, fs, commitIDPrefix) 154 if err != nil { 155 return nil, err 156 } 157 158 commitPath := fs.Join("commits", commitID[:2], commitID[2:]) 159 160 sourceMessagePath := fs.Join(commitPath, "source_message") 161 var commitMessage string 162 if dat, err := util.ReadFile(fs, sourceMessagePath); err != nil { 163 if errors.Is(err, os.ErrNotExist) { 164 return nil, status.Error(codes.NotFound, "commit info does not exist") 165 } 166 return nil, fmt.Errorf("could not open the source message file at %s, err: %w", sourceMessagePath, err) 167 } else { 168 commitMessage = string(dat) 169 } 170 171 var previousCommitMessagePath = fs.Join(commitPath, "previousCommit") 172 var previousCommitId string 173 if data, err := util.ReadFile(fs, previousCommitMessagePath); err != nil { 174 if !errors.Is(err, os.ErrNotExist) { 175 return nil, fmt.Errorf("could not open the previous commit file at %s, err: %w", previousCommitMessagePath, err) 176 } 177 } else { 178 previousCommitId = string(data) 179 } 180 181 var nextCommitMessagePath = fs.Join(commitPath, "nextCommit") 182 var nextCommitId string 183 if data, err := util.ReadFile(fs, nextCommitMessagePath); err != nil { 184 if !errors.Is(err, os.ErrNotExist) { 185 return nil, fmt.Errorf("could not open the next commit file at %s, err: %w", nextCommitMessagePath, err) 186 } //If no file exists, there is no next commit 187 } else { 188 nextCommitId = string(data) 189 } 190 191 commitApplicationsDirPath := fs.Join(commitPath, "applications") 192 dirs, err := fs.ReadDir(commitApplicationsDirPath) 193 if err != nil { 194 return nil, fmt.Errorf("could not read the applications directory at %s, error: %w", commitApplicationsDirPath, err) 195 } 196 touchedApps := make([]string, 0) 197 for _, dir := range dirs { 198 touchedApps = append(touchedApps, dir.Name()) 199 } 200 sort.Strings(touchedApps) 201 202 events, err := s.GetEvents(ctx, fs, commitPath) 203 if err != nil { 204 return nil, err 205 } 206 207 return &api.GetCommitInfoResponse{ 208 CommitHash: commitID, 209 CommitMessage: commitMessage, 210 TouchedApps: touchedApps, 211 Events: events, 212 PreviousCommitHash: previousCommitId, 213 NextCommitHash: nextCommitId, 214 }, nil 215 } 216 217 func (s *GitServer) GetEvents(ctx context.Context, fs billy.Filesystem, commitPath string) ([]*api.Event, error) { 218 var result []*api.Event 219 allEventsPath := fs.Join(commitPath, "events") 220 potentialEventDirs, err := fs.ReadDir(allEventsPath) 221 if err != nil { 222 return nil, fmt.Errorf("could not read events directory '%s': %v", allEventsPath, err) 223 } 224 for i := range potentialEventDirs { 225 oneEventDir := potentialEventDirs[i] 226 if oneEventDir.IsDir() { 227 fileName := oneEventDir.Name() 228 rawUUID, err := timeuuid.ParseUUID(fileName) 229 if err != nil { 230 return nil, fmt.Errorf("could not read event directory '%s' not a UUID: %v", fs.Join(allEventsPath, fileName), err) 231 } 232 233 var event *api.Event 234 event, err = s.ReadEvent(ctx, fs, fs.Join(allEventsPath, fileName), rawUUID) 235 if err != nil { 236 return nil, fmt.Errorf("could not read events %v", err) 237 } 238 result = append(result, event) 239 } 240 } 241 sort.Slice(result, func(i, j int) bool { 242 return result[i].CreatedAt.AsTime().UnixNano() < result[j].CreatedAt.AsTime().UnixNano() 243 }) 244 return result, nil 245 } 246 247 func (s *GitServer) ReadEvent(ctx context.Context, fs billy.Filesystem, eventPath string, eventId timeuuid.UUID) (*api.Event, error) { 248 event, err := eventmod.Read(fs, eventPath) 249 if err != nil { 250 return nil, err 251 } 252 return eventmod.ToProto(eventId, event), nil 253 } 254 255 // findCommitID checks if the "commits" directory in the given 256 // filesystem contains a commit with the given prefix. Returns the 257 // full hash of the commit, if a unique one can be found. Returns a 258 // gRPC error that can be directly returned to the client. 259 func findCommitID( 260 ctx context.Context, 261 fs billy.Filesystem, 262 commitPrefix string, 263 ) (string, error) { 264 if !valid.SHA1CommitIDPrefix(commitPrefix) { 265 return "", status.Error(codes.InvalidArgument, 266 "not a valid commit_hash") 267 } 268 commitPrefix = strings.ToLower(commitPrefix) 269 if len(commitPrefix) == valid.SHA1CommitIDLength { 270 // the easy case: the commit has been requested in 271 // full length, so we simply check if the file exist 272 // and are done. 273 commitPath := fs.Join("commits", commitPrefix[:2], commitPrefix[2:]) 274 275 if _, err := fs.Stat(commitPath); err != nil { 276 return "", grpcErrors.NotFoundError(ctx, 277 fmt.Errorf("commit %s was not found in the manifest repo", commitPrefix)) 278 } 279 280 return commitPrefix, nil 281 } 282 if len(commitPrefix) < 7 { 283 return "", status.Error(codes.InvalidArgument, 284 "commit_hash too short (must be at least 7 characters)") 285 } 286 // the dir we're looking in 287 commitDir := fs.Join("commits", commitPrefix[:2]) 288 files, err := fs.ReadDir(commitDir) 289 if err != nil { 290 return "", grpcErrors.NotFoundError(ctx, 291 fmt.Errorf("commit with prefix %s was not found in the manifest repo", commitPrefix)) 292 } 293 // the prefix of the file we're looking for 294 filePrefix := commitPrefix[2:] 295 var commitID string 296 for _, file := range files { 297 fileName := file.Name() 298 if !strings.HasPrefix(fileName, filePrefix) { 299 continue 300 } 301 if commitID != "" { 302 // another commit has already been found 303 return "", status.Error(codes.InvalidArgument, 304 "commit_hash is not unique, provide the complete hash (or a longer prefix)") 305 } 306 commitID = commitPrefix[:2] + fileName 307 } 308 if commitID == "" { 309 return "", grpcErrors.NotFoundError(ctx, 310 fmt.Errorf("commit with prefix %s was not found in the manifest repo", commitPrefix)) 311 } 312 return commitID, nil 313 }