// Copyright 2023 Harness, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package exporter import ( "bytes" "context" "crypto/tls" "encoding/json" "fmt" "io" "net/http" "strings" "github.com/harness/gitness/app/api/controller/repo" "github.com/harness/gitness/types" ) const ( pathCreateRepo = "/v1/accounts/%s/orgs/%s/projects/%s/repos" pathDeleteRepo = "/v1/accounts/%s/orgs/%s/projects/%s/repos/%s" //nolint:gosec // wrong flagging headerAPIKey = "X-Api-Key" routingID = "routingId" ) var ( errHTTPNotFound = fmt.Errorf("not found") errHTTPBadRequest = fmt.Errorf("bad request") errHTTPInternal = fmt.Errorf("internal error") errHTTPDuplicate = fmt.Errorf("resource already exists") ) type harnessCodeClient struct { client *client } type client struct { baseURL string httpClient http.Client accountID string orgID string projectID string token string } // newClient creates a new harness Client for interacting with the platforms APIs. func newClient(baseURL string, accountID string, orgID string, projectID string, token string) (*client, error) { if baseURL == "" { return nil, fmt.Errorf("baseUrl required") } if accountID == "" { return nil, fmt.Errorf("accountID required") } if orgID == "" { return nil, fmt.Errorf("orgId required") } if projectID == "" { return nil, fmt.Errorf("projectId required") } if token == "" { return nil, fmt.Errorf("token required") } return &client{ baseURL: baseURL, accountID: accountID, orgID: orgID, projectID: projectID, token: token, httpClient: http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: false, MinVersion: tls.VersionTLS12, }, }, }, }, nil } func newHarnessCodeClient( baseURL string, accountID string, orgID string, projectID string, token string, ) (*harnessCodeClient, error) { client, err := newClient(baseURL, accountID, orgID, projectID, token) if err != nil { return nil, err } return &harnessCodeClient{ client: client, }, nil } func (c *harnessCodeClient) CreateRepo(ctx context.Context, input repo.CreateInput) (*types.Repository, error) { path := fmt.Sprintf(pathCreateRepo, c.client.accountID, c.client.orgID, c.client.projectID) bodyBytes, err := json.Marshal(input) if err != nil { return nil, fmt.Errorf("failed to serialize body: %w", err) } req, err := http.NewRequestWithContext( ctx, http.MethodPost, appendPath(c.client.baseURL, path), bytes.NewBuffer(bodyBytes), ) if err != nil { return nil, fmt.Errorf("unable to create new http request : %w", err) } q := map[string]string{routingID: c.client.accountID} addQueryParams(req, q) req.Header.Add("Content-Type", "application/json") req.ContentLength = int64(len(bodyBytes)) resp, err := c.client.Do(req) if err != nil { return nil, fmt.Errorf("request execution failed: %w", err) } if resp != nil && resp.Body != nil { defer func() { _ = resp.Body.Close() }() } repository := new(types.Repository) err = mapStatusCodeToError(resp.StatusCode) if err != nil { return nil, err } err = unmarshalResponse(resp, repository) if err != nil { return nil, err } return repository, err } func addQueryParams(req *http.Request, params map[string]string) { if len(params) > 0 { q := req.URL.Query() for key, value := range params { q.Add(key, value) } req.URL.RawQuery = q.Encode() } } func (c *harnessCodeClient) DeleteRepo(ctx context.Context, repoIdentifier string) error { path := fmt.Sprintf(pathDeleteRepo, c.client.accountID, c.client.orgID, c.client.projectID, repoIdentifier) req, err := http.NewRequestWithContext(ctx, http.MethodDelete, appendPath(c.client.baseURL, path), nil) if err != nil { return fmt.Errorf("unable to create new http request : %w", err) } q := map[string]string{routingID: c.client.accountID} addQueryParams(req, q) resp, err := c.client.Do(req) if err != nil { return fmt.Errorf("request execution failed: %w", err) } if resp != nil && resp.Body != nil { defer func() { _ = resp.Body.Close() }() } return mapStatusCodeToError(resp.StatusCode) } func appendPath(uri string, path string) string { if path == "" { return uri } return strings.TrimRight(uri, "/") + "/" + strings.TrimLeft(path, "/") } func (c *client) Do(r *http.Request) (*http.Response, error) { addAuthHeader(r, c.token) return c.httpClient.Do(r) } // addAuthHeader adds the Authorization header to the request. func addAuthHeader(req *http.Request, token string) { req.Header.Add(headerAPIKey, token) } func unmarshalResponse(resp *http.Response, data interface{}) error { if resp == nil { return fmt.Errorf("http response is empty") } body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("error reading response body : %w", err) } err = json.Unmarshal(body, data) if err != nil { return fmt.Errorf("error deserializing response body : %w", err) } return nil } func mapStatusCodeToError(statusCode int) error { switch { case statusCode == 500: return errHTTPInternal case statusCode >= 500: return fmt.Errorf("received server side error status code %d", statusCode) case statusCode == 404: return errHTTPNotFound case statusCode == 400: return errHTTPBadRequest case statusCode == 409: return errHTTPDuplicate case statusCode >= 400: return fmt.Errorf("received client side error status code %d", statusCode) case statusCode >= 300: return fmt.Errorf("received further action required status code %d", statusCode) default: // TODO: definitely more things to consider here ... return nil } }