-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
use git information service on app creation and updates
This implements the usage of the git information service into `nctl`. It allows to check if the specified git repository and revision can be accessed/found.
- Loading branch information
1 parent
70d730a
commit 60c7b69
Showing
9 changed files
with
1,151 additions
and
186 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
package util | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"os" | ||
"time" | ||
|
||
apps "github.com/ninech/apis/apps/v1alpha1" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
type GitInformationClient struct { | ||
token string | ||
url *url.URL | ||
explorePath string | ||
client *http.Client | ||
logRetryFunc func(retry int, maxRetries int, err error) | ||
retryBackoffs []time.Duration | ||
} | ||
|
||
// NewGitInformationClient returns a client which can be used to retrieve | ||
// metadata information about a given git repository | ||
func NewGitInformationClient(address string, token string) (*GitInformationClient, error) { | ||
u, err := url.Parse(address) | ||
if err != nil { | ||
return nil, fmt.Errorf("can not parse git information service URL: %w", err) | ||
} | ||
return defaultGitInformationClient(setURLDefaults(u), token), nil | ||
} | ||
|
||
func setURLDefaults(u *url.URL) *url.URL { | ||
newURL := u.JoinPath("explore") | ||
if u.Scheme == "" { | ||
newURL.Scheme = "https" | ||
} | ||
return newURL | ||
} | ||
|
||
func defaultGitInformationClient(url *url.URL, token string) *GitInformationClient { | ||
return &GitInformationClient{ | ||
token: token, | ||
url: url, | ||
client: http.DefaultClient, | ||
retryBackoffs: []time.Duration{time.Second, 2 * time.Second}, | ||
} | ||
} | ||
|
||
func (g *GitInformationClient) logError(format string, v ...any) { | ||
fmt.Fprintf(os.Stderr, format, v...) | ||
} | ||
|
||
func (g *GitInformationClient) logRetry(retry int, maxRetry int, err error) { | ||
if g.logRetryFunc != nil { | ||
g.logRetryFunc(retry, maxRetry, err) | ||
return | ||
} | ||
if retry < maxRetry { | ||
g.logError("Retrying because of error: %v\n", err) | ||
} | ||
} | ||
|
||
// SetLogRetryFunc allows to set the function which logs retries when doing | ||
// requests to the git information service | ||
func (g *GitInformationClient) SetLogRetryFunc(f func(retry int, maxRetries int, err error)) { | ||
g.logRetryFunc = f | ||
} | ||
|
||
// SetRetryBackoffs sets the durations which the client will wait between retries | ||
func (g *GitInformationClient) SetRetryBackoffs(backoffs []time.Duration) { | ||
g.retryBackoffs = backoffs | ||
} | ||
|
||
func (g *GitInformationClient) repositoryInformation(ctx context.Context, url string, auth GitAuth) (*apps.GitExploreResponse, error) { | ||
req := apps.GitExploreRequest{ | ||
Repository: url, | ||
} | ||
if auth.Enabled() { | ||
req.Auth = &apps.Auth{} | ||
if auth.HasBasicAuth() { | ||
req.Auth.BasicAuth = &apps.BasicAuth{ | ||
Username: *auth.Username, | ||
Password: *auth.Password, | ||
} | ||
} | ||
if auth.HasPrivateKey() { | ||
req.Auth.PrivateKey = []byte(*auth.SSHPrivateKey) | ||
} | ||
} | ||
return g.sendRequest(ctx, req) | ||
} | ||
|
||
// RepositoryInformation returns information about a given git repository. It retries on client connection issues. | ||
func (g *GitInformationClient) RepositoryInformation(ctx context.Context, url string, auth GitAuth) (*apps.GitExploreResponse, error) { | ||
var repoInfo *apps.GitExploreResponse | ||
var err error | ||
repoInfo, err = g.repositoryInformation(ctx, url, auth) | ||
if err == nil { | ||
return repoInfo, err | ||
} | ||
g.logRetry(0, len(g.retryBackoffs), err) | ||
for retry, waitTime := range g.retryBackoffs { | ||
time.Sleep(waitTime) | ||
repoInfo, err = g.repositoryInformation(ctx, url, auth) | ||
if err == nil { | ||
break | ||
} | ||
g.logRetry(retry+1, len(g.retryBackoffs), err) | ||
} | ||
return repoInfo, err | ||
} | ||
|
||
func (g *GitInformationClient) sendRequest(ctx context.Context, req apps.GitExploreRequest) (*apps.GitExploreResponse, error) { | ||
data, err := json.Marshal(req) | ||
if err != nil { | ||
return nil, fmt.Errorf("can not JSON marshal request: %w", err) | ||
} | ||
|
||
httpReq, err := http.NewRequest("POST", g.url.String(), bytes.NewReader(data)) | ||
if err != nil { | ||
return nil, fmt.Errorf("can not create HTTP request: %w", err) | ||
} | ||
if g.token != "" { | ||
httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", g.token)) | ||
} | ||
httpReq = httpReq.WithContext(ctx) | ||
|
||
resp, err := g.client.Do(httpReq) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer resp.Body.Close() | ||
body, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return nil, fmt.Errorf("can not read response body: %w", err) | ||
} | ||
|
||
exploreResponse := &apps.GitExploreResponse{} | ||
if err := unmarshalResponse(bytes.NewReader(body), exploreResponse); err != nil { | ||
return nil, fmt.Errorf( | ||
"can not unmarshal response %q with status code %d: %w", | ||
string(body), | ||
resp.StatusCode, | ||
err, | ||
) | ||
} | ||
return exploreResponse, nil | ||
} | ||
|
||
func unmarshalResponse(data io.Reader, response *apps.GitExploreResponse) error { | ||
decoder := json.NewDecoder(data) | ||
err := decoder.Decode(response) | ||
if err != nil { | ||
return err | ||
} | ||
_, err = decoder.Token() | ||
if err == nil || !errors.Is(err, io.EOF) { | ||
return errors.New("invalid data after top-level JSON value") | ||
} | ||
return nil | ||
} | ||
|
||
// containsTag returns true of the given tag exists in the git explore response | ||
func containsTag(tag string, response *apps.GitExploreResponse) bool { | ||
if response.RepositoryInfo == nil { | ||
return false | ||
} | ||
for _, item := range response.RepositoryInfo.Tags { | ||
if item == tag { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
// containsBranch returns true of the given branch exists in the git explore response | ||
func containsBranch(branch string, response *apps.GitExploreResponse) bool { | ||
if response.RepositoryInfo == nil { | ||
return false | ||
} | ||
for _, item := range response.RepositoryInfo.Branches { | ||
if item == branch { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
// RetryLogFunc returns a retry log function depending on the given showErrors | ||
// parameter. If it is set to true, exact errors are shown when retrying to | ||
// connect to the git information service. Otherwise they are not shown. | ||
func RetryLogFunc(showErrors bool) func(retry int, maxRetries int, err error) { | ||
return func(retry int, maxRetries int, err error) { | ||
if showErrors { | ||
fmt.Fprintf(os.Stderr, "got error: %v\n", err) | ||
} | ||
if retry < maxRetries { | ||
fmt.Fprintln(os.Stderr, "Retrying...") | ||
} | ||
} | ||
} |
Oops, something went wrong.