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  }